From a0a176f07af9b34e55f13c5994e3e290f3ac7eb9 Mon Sep 17 00:00:00 2001 From: Ashish Jabble Date: Wed, 18 Oct 2023 20:24:46 +0530 Subject: [PATCH 01/54] python unit tests fixes when run in suite; issue with scheduler shared instance is not resetting on completion of test scenario (#1203) Signed-off-by: ashish-jabble --- .../core/api/control_service/test_script_management.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/python/fledge/services/core/api/control_service/test_script_management.py b/tests/unit/python/fledge/services/core/api/control_service/test_script_management.py index 596762bc91..b7d2b34517 100644 --- a/tests/unit/python/fledge/services/core/api/control_service/test_script_management.py +++ b/tests/unit/python/fledge/services/core/api/control_service/test_script_management.py @@ -84,6 +84,7 @@ async def test_get_all_scripts(self, client): with patch.object(c_mgr, 'get_category_all_items', return_value=get_cat) as patch_get_all_items: resp = await client.get('/fledge/control/script') assert 200 == resp.status + server.Server.scheduler = None result = await resp.text() json_response = json.loads(result) assert 'scripts' in json_response @@ -162,6 +163,7 @@ async def mock_manual_schedule(name): with patch.object(server.Server.scheduler, 'get_schedule_by_name', return_value=get_sch) as patch_get_schedule_by_name: resp = await client.get('/fledge/control/script/{}'.format(script_name)) + server.Server.scheduler = None assert 200 == resp.status result = await resp.text() json_response = json.loads(result) @@ -583,6 +585,7 @@ def d_schedule(*args): with patch.object(AuditLogger, 'information', return_value=arv) as audit_info_patch: resp = await client.delete('/fledge/control/script/{}'.format(script_name)) + server.Server.scheduler = None assert 200 == resp.status result = await resp.text() json_response = json.loads(result) @@ -739,6 +742,7 @@ async def test_schedule_found_for_configuration_script(self, client): with patch.object(server.Server.scheduler, 'get_schedules', return_value=get_sch) as patch_get_schedules: resp = await client.post('/fledge/control/script/{}/schedule'.format(script_name)) + server.Server.scheduler = None assert 400 == resp.status result = await resp.text() json_response = json.loads(result) @@ -788,6 +792,7 @@ async def test_schedule_configuration_for_script(self, client): with patch.object(server.Server.scheduler, 'queue_task', return_value=queue) as patch_queue_task: resp = await client.post('/fledge/control/script/{}/schedule'.format(script_name)) + server.Server.scheduler = None assert 200 == resp.status result = await resp.text() json_response = json.loads(result) From 985d64fd81ee69ddda669a58407e01b4b31907cc Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 20 Oct 2023 15:37:39 +0530 Subject: [PATCH 02/54] DOC branch reverted to develop Signed-off-by: ashish-jabble --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 18529328ca..4069ee0648 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,4 +177,4 @@ # Pass Plugin DOCBRANCH argument in Makefile ; by default develop # NOTE: During release time we need to replace DOCBRANCH with actual released version -subprocess.run(["make generated DOCBRANCH='2.2.0RC'"], shell=True, check=True) +subprocess.run(["make generated DOCBRANCH='develop'"], shell=True, check=True) From 6f9f686c9c65ed9a42eb09386d3046548051c70d Mon Sep 17 00:00:00 2001 From: Ray Verhoeff Date: Tue, 31 Oct 2023 19:33:42 -0400 Subject: [PATCH 03/54] Assign ADH credentials to OMFInformation members Signed-off-by: Ray Verhoeff --- C/plugins/north/OMF/omfinfo.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/C/plugins/north/OMF/omfinfo.cpp b/C/plugins/north/OMF/omfinfo.cpp index f9efe8b01f..427d24af08 100644 --- a/C/plugins/north/OMF/omfinfo.cpp +++ b/C/plugins/north/OMF/omfinfo.cpp @@ -159,10 +159,10 @@ OMFInformation::OMFInformation(ConfigCategory *config) : m_sender(NULL), m_omf(N m_AFMap = AFMap; // OCS configurations - OCSNamespace = OCSNamespace; - OCSTenantId = OCSTenantId; - OCSClientId = OCSClientId; - OCSClientSecret = OCSClientSecret; + m_OCSNamespace = OCSNamespace; + m_OCSTenantId = OCSTenantId; + m_OCSClientId = OCSClientId; + m_OCSClientSecret = OCSClientSecret; // PI Web API end-point - evaluates the authentication method requested if (m_PIServerEndpoint == ENDPOINT_PIWEB_API) From 581ab35661edfacdf38795be2e2aa92ed35cc6e2 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Mon, 6 Nov 2023 16:25:11 +0530 Subject: [PATCH 04/54] FOGL-8171: Short term fix Signed-off-by: Amandeep Singh Arora --- C/plugins/north/OMF/omf.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) mode change 100644 => 100755 C/plugins/north/OMF/omf.cpp diff --git a/C/plugins/north/OMF/omf.cpp b/C/plugins/north/OMF/omf.cpp old mode 100644 new mode 100755 index c4f0e4cf06..0e0bf71f3c --- a/C/plugins/north/OMF/omf.cpp +++ b/C/plugins/north/OMF/omf.cpp @@ -3031,7 +3031,15 @@ bool OMF::evaluateAFHierarchyRules(const string& assetName, const Reading& readi generateAFHierarchyPrefixLevel(m_DefaultAFLocation, prefix, AFHierarchyLevel); auto item = make_pair(m_DefaultAFLocation, prefix); - m_AssetNamePrefix[assetName].push_back(item); + auto & curr_vec = m_AssetNamePrefix[assetName]; + + // Insert new item into m_AssetNamePrefix[assetName] vector, if it doesn't exists already + if (std::find(curr_vec.begin(), curr_vec.end(), item) == curr_vec.end()) + { + m_AssetNamePrefix[assetName].push_back(item); + Logger::getLogger()->debug("m_AssetNamePrefix.size()=%d; m_AssetNamePrefix[assetName].size()=%d, added m_AssetNamePrefix[%s]=(%s,%s)", + m_AssetNamePrefix.size(), m_AssetNamePrefix[assetName].size(), assetName.c_str(), m_DefaultAFLocation.c_str(), prefix.c_str()); + } } return success; From bd2f31af9b65983698eea8de2864afc927349543 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Tue, 7 Nov 2023 16:11:33 +0000 Subject: [PATCH 05/54] FOGL-8185 Combine lookup information (#1216) * FOGL-8185 Initial implementation of a single lookup table created prior to creating any payloads to OMF in a attempt to control memory fragmentation Signed-off-by: Mark Riddoch * Some cleanup Signed-off-by: Mark Riddoch * updates Signed-off-by: Mark Riddoch * Remove debug Signed-off-by: Mark Riddoch * Review feedback Signed-off-by: Mark Riddoch * Fix logger reference Signed-off-by: Mark Riddoch * Fix typo in comment Signed-off-by: Mark Riddoch * Fix typo Signed-off-by: Mark Riddoch * Reduce function call overhead Signed-off-by: Mark Riddoch --------- Signed-off-by: Mark Riddoch --- C/common/logger.cpp | 1 + C/plugins/north/OMF/include/linkedlookup.h | 37 +++++ C/plugins/north/OMF/include/omf.h | 25 +--- C/plugins/north/OMF/include/omfinfo.h | 1 + C/plugins/north/OMF/include/omflinkeddata.h | 25 +--- C/plugins/north/OMF/linkdata.cpp | 154 ++++++++++++++++++-- C/plugins/north/OMF/omf.cpp | 14 +- C/plugins/north/OMF/omfinfo.cpp | 2 + scripts/services/north_C | 35 ++++- 9 files changed, 238 insertions(+), 56 deletions(-) create mode 100644 C/plugins/north/OMF/include/linkedlookup.h diff --git a/C/common/logger.cpp b/C/common/logger.cpp index 1470dd38be..ead4f4c0be 100755 --- a/C/common/logger.cpp +++ b/C/common/logger.cpp @@ -46,6 +46,7 @@ static char ident[80]; } openlog(ident, LOG_PID|LOG_CONS, LOG_USER); instance = this; + m_level = LOG_WARNING; } Logger::~Logger() diff --git a/C/plugins/north/OMF/include/linkedlookup.h b/C/plugins/north/OMF/include/linkedlookup.h new file mode 100644 index 0000000000..0f484efbca --- /dev/null +++ b/C/plugins/north/OMF/include/linkedlookup.h @@ -0,0 +1,37 @@ +#ifndef _LINKEDLOOKUP_H +#define _LINKEDLOOKUP_H +typedef enum { + OMFBT_UNKNOWN, OMFBT_DOUBLE64, OMFBT_DOUBLE32, OMFBT_INTEGER16, + OMFBT_INTEGER32, OMFBT_INTEGER64, OMFBT_UINTEGER16, OMFBT_UINTEGER32, + OMFBT_UINTEGER64, OMFBT_STRING, OMFBT_FLEDGEASSET +} OMFBaseType; + +/** + * Linked Asset Information class + * + * This is the data stored for each asset and asset datapoint pair that + * is being sent to PI using the linked container mechanism. We use the class + * so we can combine all the information we need in a single lookup table, + * this not only saves space but allows to build and retain the table + * before we start building the payloads. This hopefully will help prevent + * to much memory fragmentation, which was an issue with the old, separate + * lookup mechanism we had. + */ +class LALookup { + public: + LALookup() { m_sentState = 0; m_baseType = OMFBT_UNKNOWN; }; + bool assetState() { return (m_sentState & 0x01) != 0; }; + bool linkState() { return (m_sentState & 0x02) != 0; }; + bool containerState() { return (m_sentState & 0x04) != 0; }; + void setBaseType(const std::string& baseType); + OMFBaseType getBaseType() { return m_baseType; }; + std::string getBaseTypeString(); + void assetSent() { m_sentState |= 0x01; }; + void linkSent() { m_sentState |= 0x02; }; + void containerSent(const std::string& baseType); + void containerSent(OMFBaseType baseType) { m_baseType = baseType; }; + private: + uint8_t m_sentState; + OMFBaseType m_baseType; +}; +#endif diff --git a/C/plugins/north/OMF/include/omf.h b/C/plugins/north/OMF/include/omf.h index f3081d0bb6..68a382bf37 100644 --- a/C/plugins/north/OMF/include/omf.h +++ b/C/plugins/north/OMF/include/omf.h @@ -17,6 +17,7 @@ #include #include #include +#include #define OMF_HINT "OMFHint" @@ -485,25 +486,13 @@ class OMF bool m_linkedProperties; /** - * The container for this asset and data point has been sent in - * this session. + * The state of the linked assets, the key is + * either an asset name with an underscore appended + * or an asset name, followed by an underscore and a + * data point name */ - std::unordered_map - m_containerSent; - - /** - * The data message for this asset and data point has been sent in - * this session. - */ - std::unordered_map - m_assetSent; - - /** - * The link for this asset and data point has been sent in - * this session. - */ - std::unordered_map - m_linkSent; + std::unordered_map + m_linkedAssetState; /** * Force the data to be sent using the legacy, complex OMF types diff --git a/C/plugins/north/OMF/include/omfinfo.h b/C/plugins/north/OMF/include/omfinfo.h index d2d2aeae45..0e38ad09f0 100644 --- a/C/plugins/north/OMF/include/omfinfo.h +++ b/C/plugins/north/OMF/include/omfinfo.h @@ -30,6 +30,7 @@ #include "utils.h" #include "string_utils.h" #include +#include #include "crypto.hpp" diff --git a/C/plugins/north/OMF/include/omflinkeddata.h b/C/plugins/north/OMF/include/omflinkeddata.h index bf12ac4e6b..9701e93e2c 100644 --- a/C/plugins/north/OMF/include/omflinkeddata.h +++ b/C/plugins/north/OMF/include/omflinkeddata.h @@ -13,6 +13,7 @@ #include #include #include +#include /** * The OMFLinkedData class. @@ -34,13 +35,9 @@ class OMFLinkedData { public: - OMFLinkedData( std::unordered_map *containerSent, - std::unordered_map *assetSent, - std::unordered_map *linkSent, + OMFLinkedData( std::unordered_map *linkedAssetState, const OMF_ENDPOINT PIServerEndpoint = ENDPOINT_CR) : - m_containerSent(containerSent), - m_assetSent(assetSent), - m_linkSent(linkSent), + m_linkedAssetState(linkedAssetState), m_endpoint(PIServerEndpoint), m_doubleFormat("float64"), m_integerFormat("int64") @@ -48,6 +45,7 @@ class OMFLinkedData std::string processReading(const Reading& reading, const std::string& DefaultAFLocation = std::string(), OMFHints *hints = NULL); + void buildLookup(const std::vector& reading); bool flushContainers(HttpSender& sender, const std::string& path, std::vector >& header); void setFormats(const std::string& doubleFormat, const std::string& integerFormat) { @@ -77,20 +75,7 @@ class OMFLinkedData * with a '.' delimiter between. The value is the base type used, a * container will be sent if the base type changes. */ - std::unordered_map *m_containerSent; - - /** - * The data message for this asset has been sent in - * this session. The key is the asset name. The value is always true. - */ - std::unordered_map *m_assetSent; - - /** - * The link for this asset and data point has been sent in - * this session. key is the asset followed by the datapoint name - * with a '.' delimiter between. The value is always true. - */ - std::unordered_map *m_linkSent; + std::unordered_map *m_linkedAssetState; /** * The endpoint to which we are sending data diff --git a/C/plugins/north/OMF/linkdata.cpp b/C/plugins/north/OMF/linkdata.cpp index edc4771389..2c15f98392 100644 --- a/C/plugins/north/OMF/linkdata.cpp +++ b/C/plugins/north/OMF/linkdata.cpp @@ -109,7 +109,14 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi assetName = OMF::ApplyPIServerNamingRulesObj(assetName, NULL); bool needDelim = false; - if (m_assetSent->count(assetName) == 0) + auto assetLookup = m_linkedAssetState->find(assetName + "."); + if (assetLookup == m_linkedAssetState->end()) + { + // Panic Asset lookup not created + Logger::getLogger()->fatal("FIXME: no asset lookup item for %s.", assetName.c_str()); + return ""; + } + if (assetLookup->second.assetState() == false) { // Send the data message to create the asset instance outData.append("{ \"typeid\":\"FledgeAsset\", \"values\":[ { \"AssetId\":\""); @@ -117,7 +124,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi outData.append(assetName + "\""); outData.append("} ] }"); needDelim = true; - m_assetSent->insert(pair(assetName, true)); + assetLookup->second.assetSent(); } /** @@ -176,28 +183,34 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi // Create the link for the asset if not already created string link = assetName + "." + dpName; + auto dpLookup = m_linkedAssetState->find(link); + string baseType = getBaseType(dp, format); - auto container = m_containerSent->find(link); - if (container == m_containerSent->end()) + if (dpLookup == m_linkedAssetState->end()) + { + Logger::getLogger()->error("Trying to send a link for a datapoint for which we have not created a base type"); + } + else if (dpLookup->second.containerState() == false) { sendContainer(link, dp, hints, baseType); - m_containerSent->insert(pair(link, baseType)); + dpLookup->second.containerSent(baseType); } - else if (baseType.compare(container->second) != 0) + else if (baseType.compare(dpLookup->second.getBaseTypeString()) != 0) { - if (container->second.compare(0, 6, "Double") == 0 && + string bt = dpLookup->second.getBaseTypeString(); + if (bt.compare(0, 6, "Double") == 0 && (baseType.compare(0, 7, "Integer") == 0 || baseType.compare(0, 8, "UInteger") == 0)) { string msg = "Asset " + assetName + " data point " + dpName + " conversion from floating point to integer is being ignored"; OMF::reportAsset(assetName, "warn", msg); - baseType = container->second; + baseType = bt; } else { sendContainer(link, dp, hints, baseType); - (*m_containerSent)[link] = baseType; + dpLookup->second.containerSent(baseType); } } if (baseType.empty()) @@ -206,7 +219,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi skippedDatapoints.push_back(dpName); continue; } - if (m_linkSent->find(link) == m_linkSent->end()) + if (dpLookup->second.linkState() == false) { outData.append("{ \"typeid\":\"__Link\","); outData.append("\"values\":[ { \"source\" : {"); @@ -216,8 +229,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi outData.append("\"containerid\" : \""); outData.append(link); outData.append("\" } } ] },"); - - m_linkSent->insert(pair(link, true)); + dpLookup->second.linkSent(); } // Convert reading data into the OMF JSON string @@ -256,6 +268,54 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi return outData; } +/** + * If the entries are needed in the lookup table for this bblock of readings then create them + * + * @param readings A block of readings to process + */ +void OMFLinkedData::buildLookup(const vector& readings) +{ + + for (const Reading *reading : readings) + { + string assetName = reading->getAssetName(); + assetName = OMF::ApplyPIServerNamingRulesObj(assetName, NULL); + + // Apply any TagName hints to modify the containerid + LALookup empty; + + string assetKey = assetName + "."; + if (m_linkedAssetState->count(assetKey) == 0) + m_linkedAssetState->insert(pair(assetKey, empty)); + + // Get reading data + const vector data = reading->getReadingData(); + + /** + * This loop creates the data values for each of the datapoints in the + * reading. + */ + for (vector::const_iterator it = data.begin(); it != data.end(); ++it) + { + Datapoint *dp = *it; + string dpName = dp->getName(); + if (dpName.compare(OMF_HINT) == 0) + { + // Don't send the OMF Hint to the PI Server + continue; + } + dpName = OMF::ApplyPIServerNamingRulesObj(dpName, NULL); + if (!isTypeSupported(dp->getData())) + { + continue; + } + string link = assetName + "." + dpName; + if (m_linkedAssetState->count(link) == 0) + m_linkedAssetState->insert(pair(link, empty)); + } + } +} + /** * Calculate the base type we need to link the container * @@ -502,3 +562,73 @@ bool OMFLinkedData::flushContainers(HttpSender& sender, const string& path, vect } return true; } + +/** + * Set the base type by passing the string of the base type + */ +void LALookup::setBaseType(const string& baseType) +{ + if (baseType.compare("Double64") == 0) + m_baseType = OMFBT_DOUBLE64; + else if (baseType.compare("Double32") == 0) + m_baseType = OMFBT_DOUBLE32; + else if (baseType.compare("Integer16") == 0) + m_baseType = OMFBT_INTEGER16; + else if (baseType.compare("Integer32") == 0) + m_baseType = OMFBT_INTEGER32; + else if (baseType.compare("Integer64") == 0) + m_baseType = OMFBT_INTEGER64; + else if (baseType.compare("UInteger16") == 0) + m_baseType = OMFBT_UINTEGER16; + else if (baseType.compare("UInteger32") == 0) + m_baseType = OMFBT_UINTEGER32; + else if (baseType.compare("UInteger64") == 0) + m_baseType = OMFBT_UINTEGER64; + else if (baseType.compare("String") == 0) + m_baseType = OMFBT_STRING; + else if (baseType.compare("FledgeAsset") == 0) + m_baseType = OMFBT_FLEDGEASSET; + else + Logger::getLogger()->fatal("Unable to map base type '%s'", baseType.c_str()); +} + +/** + * The container has been sent with the specific base type + */ +void LALookup::containerSent(const std::string& baseType) +{ + setBaseType(baseType); + m_sentState |= 0x04; +} + +/** + * Get a string representation of the base type that was sent + */ +string LALookup::getBaseTypeString() +{ + switch (m_baseType) + { + case OMFBT_UNKNOWN: + return "Unknown"; + case OMFBT_DOUBLE64: + return "Double64"; + case OMFBT_DOUBLE32: + return "Double32"; + case OMFBT_INTEGER16: + return "Integer16"; + case OMFBT_INTEGER32: + return "Integer32"; + case OMFBT_INTEGER64: + return "Integer64"; + case OMFBT_UINTEGER16: + return "UInteger16"; + case OMFBT_UINTEGER32: + return "UInteger32"; + case OMFBT_UINTEGER64: + return "UInteger64"; + case OMFBT_STRING: + return "String"; + default: + return "Unknown"; + } +} diff --git a/C/plugins/north/OMF/omf.cpp b/C/plugins/north/OMF/omf.cpp index 0e0bf71f3c..f3a47a44f9 100755 --- a/C/plugins/north/OMF/omf.cpp +++ b/C/plugins/north/OMF/omf.cpp @@ -244,6 +244,7 @@ OMF::OMF(const string& name, m_changeTypeId = false; m_OMFDataTypes = NULL; m_OMFVersion = "1.0"; + m_connected = false; } /** @@ -271,6 +272,7 @@ OMF::OMF(const string& name, m_lastError = false; m_changeTypeId = false; + m_connected = false; } // Destructor @@ -1116,9 +1118,8 @@ uint32_t OMF::sendToServer(const vector& readings, m_baseTypesSent = true; } } - // TODO We do not need the superset stuff if we are using linked data types, - // this would save us interating over the dat aan extra time and reduce our + // this would save us iterating over the data an extra time and reduce our // memory footprint // // Create a superset of all the datapoints for each assetName @@ -1161,9 +1162,12 @@ uint32_t OMF::sendToServer(const vector& readings, bool legacyType = m_legacy; // Create the class that deals with the linked data generation - OMFLinkedData linkedData(&m_containerSent, &m_assetSent, &m_linkSent, m_PIServerEndpoint); + OMFLinkedData linkedData(&m_linkedAssetState, m_PIServerEndpoint); linkedData.setFormats(getFormatType(OMF_TYPE_FLOAT), getFormatType(OMF_TYPE_INTEGER)); + // Create the lookup data for this block of readings + linkedData.buildLookup(readings); + bool pendingSeparator = false; ostringstream jsonData; jsonData << "["; @@ -1399,10 +1403,10 @@ uint32_t OMF::sendToServer(const vector& readings, { // We do this before the send so we know if it was sent for the first time // in the processReading call - auto asset_sent = m_assetSent.find(m_assetName); + auto lookup = m_linkedAssetState.find(m_assetName + "."); // Send data for this reading using the new mechanism outData = linkedData.processReading(*reading, AFHierarchyPrefix, hints); - if (m_sendFullStructure && asset_sent == m_assetSent.end()) + if (m_sendFullStructure && lookup->second.assetState() == false) { // If the hierarchy has not already been sent then send it if (! AFHierarchySent) diff --git a/C/plugins/north/OMF/omfinfo.cpp b/C/plugins/north/OMF/omfinfo.cpp index 427d24af08..ed3c5b7b98 100644 --- a/C/plugins/north/OMF/omfinfo.cpp +++ b/C/plugins/north/OMF/omfinfo.cpp @@ -461,6 +461,7 @@ uint32_t OMFInformation::send(const vector& readings) { // Created a new sender after a connection failure m_omf->setSender(*m_sender); + m_omf->setConnected(false); } } @@ -536,6 +537,7 @@ uint32_t OMFInformation::send(const vector& readings) Logger::getLogger()->warn("Connection to PI Web API at %s has been lost", m_hostAndPort.c_str()); } m_connected = updatedConnected; + #if INSTRUMENT Logger::getLogger()->debug("plugin_send elapsed time: %6.3f seconds, NumValues: %u", GetElapsedTime(&startTime), ret); diff --git a/scripts/services/north_C b/scripts/services/north_C index 80fa8a7968..3cf728d425 100755 --- a/scripts/services/north_C +++ b/scripts/services/north_C @@ -27,11 +27,44 @@ if [ "$VALGRIND_NORTH" != "" ]; then done fi +runstrace=n +if [ "$STRACE_NORTH" != "" ]; then + for i in "$@"; do + case $i in + --name=*) + name="`echo $i | sed -e s/--name=//`" + ;; + esac + done + services=$(echo $STRACE_NORTH | tr ";" "\n") + for service in $services; do + if [ "$service" = "$name" ]; then + runstrace=y + fi + done +fi + cd "${FLEDGE_ROOT}/services" if [ "$runvalgrind" = "y" ]; then file=${HOME}/north.${name}.valgrind.out rm -f $file - valgrind --leak-check=full --trace-children=yes --show-leak-kinds=all --track-origins=yes --log-file=$file ./fledge.services.north "$@" + logger "Running north service $name under valgrind" + if [ "$VALGRIND_MASSIF" != "" ]; then + valgrind --tool=massif --detailed-freq=1 --pages-as-heap=yes ./fledge.services.north "$@" + else + valgrind --leak-check=full --trace-children=yes --show-leak-kinds=all --track-origins=yes --log-file=$file ./fledge.services.north "$@" + fi +elif [ "$runstrace" = "y" ]; then + file=${HOME}/north.${name}.strace.out + logger "Running north service $name under strace" + rm -f $file + strace -e 'trace=%memory,%process,%file' -f -o $file ./fledge.services.north "$@" +elif [ "$INTERPOSE_NORTH" != "" ]; then + LD_PRELOAD=${INTERPOSE_NORTH} + logger "Running north service with interpose library $INTERPOSE_NORTH" + export LD_PRELOAD + ./fledge.services.north "$@" + unset LD_PRELOAD else ./fledge.services.north "$@" fi From 669e3b1a9ee7466409298607ccc5d12d080b4b6d Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 12:15:12 +0530 Subject: [PATCH 06/54] skeleton added for performance monitor API Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 python/fledge/services/core/api/performance_monitor.py diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py new file mode 100644 index 0000000000..d7e303ace8 --- /dev/null +++ b/python/fledge/services/core/api/performance_monitor.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +# FLEDGE_BEGIN +# See: http://fledge-iot.readthedocs.io/ +# FLEDGE_END + +from aiohttp import web + +from fledge.common.logger import FLCoreLogger + +__author__ = "Ashish Jabble" +__copyright__ = "Copyright (c) 2023, Dianomic Systems Inc." +__license__ = "Apache 2.0" +__version__ = "${VERSION}" + +_help = """ + ---------------------------------------------------------------- + | GET DELETE | /fledge/monitors | + | GET DELETE | /fledge/monitors/{service} | + | GET DELETE | /fledge/monitors/{service}/{counter} | + ---------------------------------------------------------------- +""" +_LOGGER = FLCoreLogger().get_logger(__name__) + +def setup(app): + app.router.add_route('GET', '/fledge/monitors', get_all) + app.router.add_route('GET', '/fledge/monitors/{service}', get_by_service_name) + app.router.add_route('GET', '/fledge/monitors/{service}/{counter}', get_by_service_and_counter_name) + app.router.add_route('DELETE', '/fledge/monitors', purge_all) + app.router.add_route('DELETE', '/fledge/monitors/{service}', purge_by_service) + app.router.add_route('DELETE', '/fledge/monitors/{service}/{counter}', purge_by_service_and_counter) + +async def get_all(request: web.Request) -> web.Response: + """ GET list of performance monitors + + :Example: + curl -sX GET http://localhost:8081/fledge/monitors + """ + return web.json_response({}) + + +async def get_by_service_name(request: web.Request) -> web.Response: + """ GET performance monitors for the given service + + :Example: + curl -sX GET http://localhost:8081/fledge/monitors/ + """ + return web.json_response({}) + +async def get_by_service_and_counter_name(request: web.Request) -> web.Response: + """ GET values for the single counter for the single service + + :Example: + curl -sX GET http://localhost:8081/fledge/monitors// + """ + return web.json_response({}) + + +async def purge_all(request: web.Request) -> web.Response: + """ DELETE all performance monitors + + :Example: + curl -sX DELETE http://localhost:8081/fledge/monitors + """ + return web.json_response({}) + +async def purge_by_service(request: web.Request) -> web.Response: + """ DELETE performance monitors for the given service + + :Example: + curl -sX DELETE http://localhost:8081/fledge/monitors/ + """ + return web.json_response({}) + +async def purge_by_service_and_counter(request: web.Request) -> web.Response: + """ DELETE performance monitors for the single counter for the single service + + :Example: + curl -sX DELETE http://localhost:8081/fledge/monitors// + """ + return web.json_response({}) From 6a768c45b94d5be0713edc64acf30ea0e4227862 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 12:20:16 +0530 Subject: [PATCH 07/54] performance monitor routes available to Public API Signed-off-by: ashish-jabble --- python/fledge/services/core/routes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/fledge/services/core/routes.py b/python/fledge/services/core/routes.py index b356008e63..469d66af30 100644 --- a/python/fledge/services/core/routes.py +++ b/python/fledge/services/core/routes.py @@ -5,7 +5,7 @@ # FLEDGE_END from fledge.services.core import proxy -from fledge.services.core.api import asset_tracker, auth, backup_restore, browser, certificate_store, filters, health, notification, north, package_log, python_packages, south, support, service, task, update +from fledge.services.core.api import asset_tracker, auth, backup_restore, browser, certificate_store, filters, health, notification, north, package_log, performance_monitor, python_packages, south, support, service, task, update from fledge.services.core.api import audit as api_audit from fledge.services.core.api import common as api_common from fledge.services.core.api import configuration as api_configuration @@ -264,6 +264,9 @@ def setup(app): # Proxy Admin API setup with regex proxy.admin_api_setup(app) + # Performance Monitor + performance_monitor.setup(app) + # enable cors support enable_cors(app) From e09194ea2c32887191cc5e8e2edf569979c8ca85 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 12:57:57 +0530 Subject: [PATCH 08/54] get specific counter for a specific service API addition Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index d7e303ace8..152cd2a410 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -7,6 +7,8 @@ from aiohttp import web from fledge.common.logger import FLCoreLogger +from fledge.common.storage_client.payload_builder import PayloadBuilder +from fledge.services.core import connect __author__ = "Ashish Jabble" __copyright__ = "Copyright (c) 2023, Dianomic Systems Inc." @@ -53,8 +55,19 @@ async def get_by_service_and_counter_name(request: web.Request) -> web.Response: :Example: curl -sX GET http://localhost:8081/fledge/monitors// """ - return web.json_response({}) - + service = request.match_info.get('service', None) + counter = request.match_info.get('counter', None) + + storage = connect.get_storage_async() + payload = PayloadBuilder().SELECT("average", "maximum", "minimum", "samples", "ts").ALIAS( + "return", ("ts", 'timestamp')).FORMAT("return", ("ts", "YYYY-MM-DD HH24:MI:SS.MS")).WHERE( + ["service", '=', service]).AND_WHERE(["monitor", '=', counter]).payload() + result = await storage.query_tbl_with_payload('monitors', payload) + response = {} + if 'rows' in result: + response = {"service": service, "monitors":{"monitor": counter}} + response["monitors"]["values"] = result["rows"] if result["rows"] else [] + return web.json_response(response) async def purge_all(request: web.Request) -> web.Response: """ DELETE all performance monitors From a0af03b022308d55adf6922dba85ce4e373e7693 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 15:19:40 +0530 Subject: [PATCH 09/54] get all counters for a specific service API addition Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index 152cd2a410..16924e7452 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -47,7 +47,22 @@ async def get_by_service_name(request: web.Request) -> web.Response: :Example: curl -sX GET http://localhost:8081/fledge/monitors/ """ - return web.json_response({}) + service = request.match_info.get('service', None) + storage = connect.get_storage_async() + payload = PayloadBuilder().SELECT("average", "maximum", "minimum", "monitor", "samples", "ts").ALIAS( + "return", ("ts", 'timestamp')).FORMAT("return", ("ts", "YYYY-MM-DD HH24:MI:SS.MS")).WHERE( + ["service", '=', service]).payload() + response = {"service": service} + result = await storage.query_tbl_with_payload('monitors', payload) + if 'rows' in result: + monitor = {} + for d in result["rows"]: + val = {"average": d["average"], "maximum": d["maximum"], "minimum": d["minimum"], "samples": d["samples"], + "timestamp": d["timestamp"]} + monitor.setdefault(d['monitor'], []).append(val) + monitors = [{'monitor': k, 'values': v} for k, v in monitor.items()] + response["monitors"] = monitors + return web.json_response(response) async def get_by_service_and_counter_name(request: web.Request) -> web.Response: """ GET values for the single counter for the single service From bdaeb404c4caefde8a51536149b38e56de05ea75 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 15:35:39 +0530 Subject: [PATCH 10/54] Purge/Remove particular counter API addition Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index 16924e7452..c312002e2c 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -38,7 +38,7 @@ async def get_all(request: web.Request) -> web.Response: :Example: curl -sX GET http://localhost:8081/fledge/monitors """ - return web.json_response({}) + return web.json_response({"message": "To be Implemented"}) async def get_by_service_name(request: web.Request) -> web.Response: @@ -90,7 +90,7 @@ async def purge_all(request: web.Request) -> web.Response: :Example: curl -sX DELETE http://localhost:8081/fledge/monitors """ - return web.json_response({}) + return web.json_response({"message": "To be Implemented"}) async def purge_by_service(request: web.Request) -> web.Response: """ DELETE performance monitors for the given service @@ -98,7 +98,7 @@ async def purge_by_service(request: web.Request) -> web.Response: :Example: curl -sX DELETE http://localhost:8081/fledge/monitors/ """ - return web.json_response({}) + return web.json_response({"message": "To be Implemented"}) async def purge_by_service_and_counter(request: web.Request) -> web.Response: """ DELETE performance monitors for the single counter for the single service @@ -106,4 +106,13 @@ async def purge_by_service_and_counter(request: web.Request) -> web.Response: :Example: curl -sX DELETE http://localhost:8081/fledge/monitors// """ - return web.json_response({}) + service = request.match_info.get('service', None) + counter = request.match_info.get('counter', None) + storage = connect.get_storage_async() + payload = PayloadBuilder().WHERE(["service", '=', service]).AND_WHERE( + ["monitor", '=', counter]).payload() + result = await storage.delete_from_tbl("monitors", payload) + message = "Nothing to remove '{}' counter from '{}' service.".format(counter, service) + if result['rows_affected']: + message = "Performance '{}' counter has been removed from '{}' service.".format(counter, service) + return web.json_response({"message": message}) From 306267cb1517028c0e9e5ff6073626bf7a593f7d Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 15:40:50 +0530 Subject: [PATCH 11/54] Purge/Remove service counters API addition Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index c312002e2c..2944046654 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -98,7 +98,15 @@ async def purge_by_service(request: web.Request) -> web.Response: :Example: curl -sX DELETE http://localhost:8081/fledge/monitors/ """ - return web.json_response({"message": "To be Implemented"}) + service = request.match_info.get('service', None) + storage = connect.get_storage_async() + payload = PayloadBuilder().WHERE(["service", '=', service]).payload() + result = await storage.delete_from_tbl("monitors", payload) + message = "Nothing to remove counters from '{}' service.".format(service) + if 'rows_affected' in result: + if result['rows_affected']: + message = "Performance counters have been removed from '{}' service.".format(service) + return web.json_response({"message": message}) async def purge_by_service_and_counter(request: web.Request) -> web.Response: """ DELETE performance monitors for the single counter for the single service @@ -113,6 +121,7 @@ async def purge_by_service_and_counter(request: web.Request) -> web.Response: ["monitor", '=', counter]).payload() result = await storage.delete_from_tbl("monitors", payload) message = "Nothing to remove '{}' counter from '{}' service.".format(counter, service) - if result['rows_affected']: - message = "Performance '{}' counter has been removed from '{}' service.".format(counter, service) + if 'rows_affected' in result: + if result['rows_affected']: + message = "Performance '{}' counter has been removed from '{}' service.".format(counter, service) return web.json_response({"message": message}) From 121b8ee1604a115ad91b9434c10b28dc0c5ebaeb Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 15:56:58 +0530 Subject: [PATCH 12/54] Purge/Remove all service counters API addition Signed-off-by: ashish-jabble --- .../fledge/services/core/api/performance_monitor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index 2944046654..e56e12fb4f 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -90,7 +90,13 @@ async def purge_all(request: web.Request) -> web.Response: :Example: curl -sX DELETE http://localhost:8081/fledge/monitors """ - return web.json_response({"message": "To be Implemented"}) + storage = connect.get_storage_async() + result = await storage.delete_from_tbl("monitors", {}) + message = "Nothing to remove for service performance counters." + if 'rows_affected' in result: + if result['response'] == "deleted" and result['rows_affected']: + message = "All Performance counters have been removed successfully." + return web.json_response({"message": message}) async def purge_by_service(request: web.Request) -> web.Response: """ DELETE performance monitors for the given service @@ -104,7 +110,7 @@ async def purge_by_service(request: web.Request) -> web.Response: result = await storage.delete_from_tbl("monitors", payload) message = "Nothing to remove counters from '{}' service.".format(service) if 'rows_affected' in result: - if result['rows_affected']: + if result['response'] == "deleted" and result['rows_affected']: message = "Performance counters have been removed from '{}' service.".format(service) return web.json_response({"message": message}) @@ -122,6 +128,6 @@ async def purge_by_service_and_counter(request: web.Request) -> web.Response: result = await storage.delete_from_tbl("monitors", payload) message = "Nothing to remove '{}' counter from '{}' service.".format(counter, service) if 'rows_affected' in result: - if result['rows_affected']: + if result['response'] == "deleted" and result['rows_affected']: message = "Performance '{}' counter has been removed from '{}' service.".format(counter, service) return web.json_response({"message": message}) From 521d8240bf1ade7e44272a5a08180c4ccaa64034 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Fri, 17 Nov 2023 11:08:06 +0000 Subject: [PATCH 13/54] FOGL-8260 Fix sendign AFLocation information (#1221) Signed-off-by: Mark Riddoch --- C/plugins/north/OMF/include/linkedlookup.h | 20 +++++++++++++++----- C/plugins/north/OMF/linkdata.cpp | 2 +- C/plugins/north/OMF/omf.cpp | 8 ++++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/C/plugins/north/OMF/include/linkedlookup.h b/C/plugins/north/OMF/include/linkedlookup.h index 0f484efbca..a38cdbba2a 100644 --- a/C/plugins/north/OMF/include/linkedlookup.h +++ b/C/plugins/north/OMF/include/linkedlookup.h @@ -6,6 +6,14 @@ typedef enum { OMFBT_UINTEGER64, OMFBT_STRING, OMFBT_FLEDGEASSET } OMFBaseType; +/** + * Lookup status bit + */ +#define LAL_ASSET_SENT 0x01 // We have sent the asset +#define LAL_LINK_SENT 0x02 // We have sent the link to the base type +#define LAL_CONTAINER_SENT 0x04 // We have sent the container +#define LAL_AFLINK_SENT 0x08 // We have sent the link to the AF location + /** * Linked Asset Information class * @@ -20,14 +28,16 @@ typedef enum { class LALookup { public: LALookup() { m_sentState = 0; m_baseType = OMFBT_UNKNOWN; }; - bool assetState() { return (m_sentState & 0x01) != 0; }; - bool linkState() { return (m_sentState & 0x02) != 0; }; - bool containerState() { return (m_sentState & 0x04) != 0; }; + bool assetState() { return (m_sentState & LAL_ASSET_SENT) != 0; }; + bool linkState() { return (m_sentState & LAL_LINK_SENT) != 0; }; + bool containerState() { return (m_sentState & LAL_CONTAINER_SENT) != 0; }; + bool afLinkState() { return (m_sentState & LAL_AFLINK_SENT) != 0; }; void setBaseType(const std::string& baseType); OMFBaseType getBaseType() { return m_baseType; }; std::string getBaseTypeString(); - void assetSent() { m_sentState |= 0x01; }; - void linkSent() { m_sentState |= 0x02; }; + void assetSent() { m_sentState |= LAL_ASSET_SENT; }; + void linkSent() { m_sentState |= LAL_LINK_SENT; }; + void afLinkSent() { m_sentState |= LAL_AFLINK_SENT; }; void containerSent(const std::string& baseType); void containerSent(OMFBaseType baseType) { m_baseType = baseType; }; private: diff --git a/C/plugins/north/OMF/linkdata.cpp b/C/plugins/north/OMF/linkdata.cpp index 2c15f98392..ce1a61bdb3 100644 --- a/C/plugins/north/OMF/linkdata.cpp +++ b/C/plugins/north/OMF/linkdata.cpp @@ -598,7 +598,7 @@ void LALookup::setBaseType(const string& baseType) void LALookup::containerSent(const std::string& baseType) { setBaseType(baseType); - m_sentState |= 0x04; + m_sentState |= LAL_CONTAINER_SENT; } /** diff --git a/C/plugins/north/OMF/omf.cpp b/C/plugins/north/OMF/omf.cpp index f3a47a44f9..53443787e0 100755 --- a/C/plugins/north/OMF/omf.cpp +++ b/C/plugins/north/OMF/omf.cpp @@ -1406,7 +1406,7 @@ uint32_t OMF::sendToServer(const vector& readings, auto lookup = m_linkedAssetState.find(m_assetName + "."); // Send data for this reading using the new mechanism outData = linkedData.processReading(*reading, AFHierarchyPrefix, hints); - if (m_sendFullStructure && lookup->second.assetState() == false) + if (m_sendFullStructure && lookup->second.afLinkState() == false) { // If the hierarchy has not already been sent then send it if (! AFHierarchySent) @@ -1425,6 +1425,7 @@ uint32_t OMF::sendToServer(const vector& readings, outData.append(","); outData.append(af); } + lookup->second.afLinkSent(); } } if (!outData.empty()) @@ -4607,7 +4608,10 @@ std::string OMF::ApplyPIServerNamingRulesObj(const std::string &objName, bool *c nameFixed = StringTrim(objName); - Logger::getLogger()->debug("%s - original :%s: trimmed :%s:", __FUNCTION__, objName.c_str(), nameFixed.c_str()); + if (objName.compare(nameFixed) != 0) + { + Logger::getLogger()->debug("%s - original :%s: trimmed :%s:", __FUNCTION__, objName.c_str(), nameFixed.c_str()); + } if (nameFixed.empty ()) { From 83dd995f5019ec5a45e8385bfa15915798215e18 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 17:20:15 +0530 Subject: [PATCH 14/54] Get All Performance Counters API addition Signed-off-by: ashish-jabble --- .../services/core/api/performance_monitor.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/python/fledge/services/core/api/performance_monitor.py b/python/fledge/services/core/api/performance_monitor.py index e56e12fb4f..9a983b2fc3 100644 --- a/python/fledge/services/core/api/performance_monitor.py +++ b/python/fledge/services/core/api/performance_monitor.py @@ -32,13 +32,26 @@ def setup(app): app.router.add_route('DELETE', '/fledge/monitors/{service}', purge_by_service) app.router.add_route('DELETE', '/fledge/monitors/{service}/{counter}', purge_by_service_and_counter) +# TODO: 8167 - Limit and Offset support and other pending queries + async def get_all(request: web.Request) -> web.Response: """ GET list of performance monitors :Example: curl -sX GET http://localhost:8081/fledge/monitors """ - return web.json_response({"message": "To be Implemented"}) + storage = connect.get_storage_async() + monitors = await storage.query_tbl("monitors") + counters = monitors["rows"] + monitor = {} + response = {} + for c in counters: + val = {"average": c["average"], "maximum": c["maximum"], "minimum": c["minimum"], "samples": c["samples"], + "timestamp": c["ts"], "service": c["service"]} + monitor.setdefault(c['monitor'], []).append(val) + monitors = [{'monitor': k, 'values': v} for k, v in monitor.items()] + response["monitors"] = monitors + return web.json_response(response) async def get_by_service_name(request: web.Request) -> web.Response: @@ -56,10 +69,10 @@ async def get_by_service_name(request: web.Request) -> web.Response: result = await storage.query_tbl_with_payload('monitors', payload) if 'rows' in result: monitor = {} - for d in result["rows"]: - val = {"average": d["average"], "maximum": d["maximum"], "minimum": d["minimum"], "samples": d["samples"], - "timestamp": d["timestamp"]} - monitor.setdefault(d['monitor'], []).append(val) + for row in result["rows"]: + val = {"average": row["average"], "maximum": row["maximum"], "minimum": row["minimum"], + "samples": row["samples"], "timestamp": row["timestamp"]} + monitor.setdefault(row['monitor'], []).append(val) monitors = [{'monitor': k, 'values': v} for k, v in monitor.items()] response["monitors"] = monitors return web.json_response(response) From 071007620bda5194a0592a64de448638907a55ab Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Fri, 17 Nov 2023 11:58:59 +0000 Subject: [PATCH 15/54] FOGL-8222 Improve error and warnign reporting when updates and inserts (#1217) require retries Signed-off-by: Mark Riddoch --- .../storage/sqlite/common/connection.cpp | 30 ++++++++++++------- C/services/north/data_load.cpp | 9 +++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/C/plugins/storage/sqlite/common/connection.cpp b/C/plugins/storage/sqlite/common/connection.cpp index 989e607c31..1a989c864b 100644 --- a/C/plugins/storage/sqlite/common/connection.cpp +++ b/C/plugins/storage/sqlite/common/connection.cpp @@ -26,7 +26,7 @@ #define PURGE_SLOWDOWN_AFTER_BLOCKS 5 #define PURGE_SLOWDOWN_SLEEP_MS 500 -#define LOG_AFTER_NERRORS 5 +#define LOG_AFTER_NERRORS (MAX_RETRIES / 2) /** * SQLite3 storage plugin for Fledge @@ -3133,8 +3133,6 @@ int retries = 0, rc; retries++; if (rc != SQLITE_OK) { - if (retries > LOG_AFTER_NERRORS) - Logger::getLogger()->warn("Connection::SQLexec - retry :%d: dbHandle :%X: cmd :%s: error :%s:", retries, this->getDbHandle(), sql, sqlite3_errmsg(dbHandle)); #if DO_PROFILE_RETRIES @@ -3146,8 +3144,6 @@ int retries = 0, rc; #endif int interval = (1 * RETRY_BACKOFF); std::this_thread::sleep_for(std::chrono::milliseconds(interval)); - if (retries > 9) Logger::getLogger()->info("SQLExec: error :%s: retry %d of %d, rc=%s, errmsg=%s, DB connection @ %p, slept for %d msecs", - sqlite3_errmsg(dbHandle), retries, MAX_RETRIES, (rc==SQLITE_LOCKED)?"SQLITE_LOCKED":"SQLITE_BUSY", sqlite3_errmsg(db), this, interval); #if DO_PROFILE_RETRIES m_qMutex.lock(); m_waiting.fetch_sub(1); @@ -3171,6 +3167,15 @@ int retries = 0, rc; } } while (retries < MAX_RETRIES && (rc != SQLITE_OK)); + if (retries >= MAX_RETRIES) + { + Logger::getLogger()->error("SQL statement %s failed after maximum retries", sql, sqlite3_errmsg(dbHandle)); + } + else if (retries > LOG_AFTER_NERRORS) + { + Logger::getLogger()->warn("%d retries required of the SQL statement '%s': %s", retries, sql, sqlite3_errmsg(dbHandle)); + Logger::getLogger()->warn("If the excessive retries continue for sustained periods it is a sign that the system may be reaching the limits of the load it can handle"); + } #if DO_PROFILE_RETRIES retryStats[retries-1]++; if (++numStatements > RETRY_REPORT_THRESHOLD - 1) @@ -3240,14 +3245,17 @@ int retries = 0, rc; int interval = (retries * RETRY_BACKOFF); this_thread::sleep_for(chrono::milliseconds(interval)); - - if (retries > 5) { - Logger::getLogger()->debug("SQLStep: retry %d of %d, rc=%s, DB connection @ %p, slept for %d msecs", - retries, MAX_RETRIES, (rc==SQLITE_LOCKED)?"SQLITE_LOCKED":"SQLITE_BUSY", this, interval); - - } } } while (retries < MAX_RETRIES && (rc == SQLITE_LOCKED || rc == SQLITE_BUSY)); + if (retries >= MAX_RETRIES) + { + Logger::getLogger()->error("SQL statement failed after maximum retries", sqlite3_errmsg(dbHandle)); + } + else if (retries > LOG_AFTER_NERRORS) + { + Logger::getLogger()->warn("%d retries required of the SQL statement: %s", retries, sqlite3_errmsg(dbHandle)); + Logger::getLogger()->warn("If the excessive retries continue for sustained periods it is a sign that the system may be reaching the limits of the load it can handle"); + } #if DO_PROFILE_RETRIES retryStats[retries-1]++; if (++numStatements > 1000) diff --git a/C/services/north/data_load.cpp b/C/services/north/data_load.cpp index 77567dad13..da15df6c0e 100755 --- a/C/services/north/data_load.cpp +++ b/C/services/north/data_load.cpp @@ -604,7 +604,14 @@ void DataLoad::updateStatistic(const string& key, const string& description, uin if (m_storage->insertTable(table, values) != 1) { - Logger::getLogger()->error("Failed to insert a new row into the %s", table.c_str()); + if (m_storage->updateTable("statistics", updateValue, wLastStat) == 1) + { + Logger::getLogger()->warn("Statistics update has suceeded, the above failures are the likely result of a race condition between services and can be ignored"); + } + else + { + Logger::getLogger()->error("Failed to insert a new row into the %s", table.c_str()); + } } else { From eab8654adf513a6423920ff8ff9d265a06c82c49 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Fri, 17 Nov 2023 20:24:41 +0530 Subject: [PATCH 16/54] bucket type and properties optional attribute support added in configuration manager Signed-off-by: ashish-jabble --- python/fledge/common/configuration_manager.py | 6 +++--- .../unit/python/fledge/common/test_configuration_manager.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/fledge/common/configuration_manager.py b/python/fledge/common/configuration_manager.py index ecbc8cb4c0..99326350d3 100644 --- a/python/fledge/common/configuration_manager.py +++ b/python/fledge/common/configuration_manager.py @@ -35,7 +35,7 @@ # MAKE UPPER_CASE _valid_type_strings = sorted(['boolean', 'integer', 'float', 'string', 'IPv4', 'IPv6', 'X509 certificate', 'password', - 'JSON', 'URL', 'enumeration', 'script', 'code', 'northTask', 'ACL']) + 'JSON', 'URL', 'enumeration', 'script', 'code', 'northTask', 'ACL', 'bucket']) _optional_items = sorted(['readonly', 'order', 'length', 'maximum', 'minimum', 'rule', 'deprecated', 'displayName', 'validity', 'mandatory', 'group']) RESERVED_CATG = ['South', 'North', 'General', 'Advanced', 'Utilities', 'rest_api', 'Security', 'service', 'SCHEDULER', @@ -268,7 +268,7 @@ async def _validate_category_val(self, category_name, category_val, set_value_va optional_item_entries = {'readonly': 0, 'order': 0, 'length': 0, 'maximum': 0, 'minimum': 0, 'deprecated': 0, 'displayName': 0, 'rule': 0, 'validity': 0, 'mandatory': 0, - 'group': 0} + 'group': 0, 'properties': 0} expected_item_entries = {'description': 0, 'default': 0, 'type': 0} if require_entry_value: @@ -332,7 +332,7 @@ def get_entry_val(k): entry_val)) is False: raise ValueError('For {} category, entry value must be an integer or float for item name ' '{}; got {}'.format(category_name, entry_name, type(entry_val))) - elif entry_name in ('displayName', 'group', 'rule', 'validity'): + elif entry_name in ('displayName', 'group', 'rule', 'validity', 'properties'): if not isinstance(entry_val, str): raise ValueError('For {} category, entry value must be string for item name {}; got {}' .format(category_name, entry_name, type(entry_val))) diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index 5048a75f03..aa87be20e5 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -35,7 +35,7 @@ def reset_singleton(self): def test_supported_validate_type_strings(self): expected_types = ['IPv4', 'IPv6', 'JSON', 'URL', 'X509 certificate', 'boolean', 'code', 'enumeration', 'float', 'integer', - 'northTask', 'password', 'script', 'string', 'ACL'] + 'northTask', 'password', 'script', 'string', 'ACL', 'bucket'] assert len(expected_types) == len(_valid_type_strings) assert sorted(expected_types) == _valid_type_strings From 08e4edbb11e6bdaf120515da17546c8e3165d4ea Mon Sep 17 00:00:00 2001 From: Mohit04tomar <43023917+Mohit04tomar@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:20:19 +0530 Subject: [PATCH 17/54] System test fixes (#1220) --- .../system/python/packages/test_north_pi_webapi_nw_throttle.py | 2 +- tests/system/python/packages/test_rule_data_availability.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/system/python/packages/test_north_pi_webapi_nw_throttle.py b/tests/system/python/packages/test_north_pi_webapi_nw_throttle.py index 42edec7ddd..e52dba4d93 100644 --- a/tests/system/python/packages/test_north_pi_webapi_nw_throttle.py +++ b/tests/system/python/packages/test_north_pi_webapi_nw_throttle.py @@ -329,7 +329,7 @@ def test_omf_in_impaired_network(self, clean_setup_fledge_packages, reset_fledge raise Exception("None of packet delay or rate limit given, " "cannot apply network impairment.") # Insert some readings before turning off compression. - time.sleep(2) + time.sleep(3) # Turn off south service disable_schedule(fledge_url, SOUTH_SERVICE_NAME) time.sleep(5) diff --git a/tests/system/python/packages/test_rule_data_availability.py b/tests/system/python/packages/test_rule_data_availability.py index 38faa3b97a..2561685af1 100644 --- a/tests/system/python/packages/test_rule_data_availability.py +++ b/tests/system/python/packages/test_rule_data_availability.py @@ -263,6 +263,7 @@ def test_data_availability_north(self, check_eds_installed, reset_fledge, start_ get_url = "/fledge/audit?source=NTFSN" resp1 = utils.get_request(fledge_url, get_url) + time.sleep(wait_time) get_url = "/fledge/audit?source=NTFSN" resp2 = utils.get_request(fledge_url, get_url) assert len(resp2['audit']) > len(resp1['audit']), "ERROR: NTFSN not triggered properly with asset code" From dfbb81fc5abe29b567430d911c1dd6cc03f3a3af Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Mon, 20 Nov 2023 13:41:47 +0530 Subject: [PATCH 18/54] more unit tests updated & optional properties item rejected on update Signed-off-by: ashish-jabble --- python/fledge/common/configuration_manager.py | 11 +++++++---- .../fledge/common/test_configuration_manager.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/python/fledge/common/configuration_manager.py b/python/fledge/common/configuration_manager.py index 99326350d3..0d9ac9ad46 100644 --- a/python/fledge/common/configuration_manager.py +++ b/python/fledge/common/configuration_manager.py @@ -968,14 +968,17 @@ async def set_optional_value_entry(self, category_name, item_name, optional_entr return # Validate optional types only when new_value_entry not empty; otherwise set empty value if new_value_entry: - if optional_entry_name == 'readonly' or optional_entry_name == 'deprecated' or optional_entry_name == 'mandatory': + if optional_entry_name == "properties": + raise ValueError('For {} category, optional item name properties cannot be updated.'.format( + category_name)) + elif optional_entry_name in ('readonly', 'deprecated', 'mandatory'): if self._validate_type_value('boolean', new_value_entry) is False: raise ValueError( 'For {} category, entry value must be boolean for optional item name {}; got {}' .format(category_name, optional_entry_name, type(new_value_entry))) - elif optional_entry_name == 'minimum' or optional_entry_name == 'maximum': - if (self._validate_type_value('integer', new_value_entry) or self._validate_type_value('float', - new_value_entry)) is False: + elif optional_entry_name in ('minimum', 'maximum'): + if (self._validate_type_value('integer', new_value_entry) or self._validate_type_value( + 'float', new_value_entry)) is False: raise ValueError('For {} category, entry value must be an integer or float for optional item ' '{}; got {}'.format(category_name, optional_entry_name, type(new_value_entry))) elif optional_entry_name in ('displayName', 'group', 'rule', 'validity'): diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index aa87be20e5..79d910f8df 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -539,11 +539,15 @@ async def test__validate_category_val_enum_type_bad(self, config, exception_name ("string", " ", False), ("JSON", "", False), ("JSON", " ", False), + ("bucket", "", False), + ("bucket", " ", False), ("integer", " ", True), ("string", "", True), ("string", " ", True), ("JSON", "", True), - ("JSON", " ", True) + ("JSON", " ", True), + ("bucket", "", True), + ("bucket", " ", True) ]) async def test__validate_category_val_with_optional_mandatory(self, _type, value, from_default_val): storage_client_mock = MagicMock(spec=StorageClientAsync) @@ -554,7 +558,8 @@ async def test__validate_category_val_with_optional_mandatory(self, _type, value await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=test_config, set_value_val_from_default_val=from_default_val) assert excinfo.type is ValueError - assert "For {} category, A default value must be given for {}".format(CAT_NAME, ITEM_NAME) == str(excinfo.value) + assert ("For {} category, A default value must be given for {}" + "").format(CAT_NAME, ITEM_NAME) == str(excinfo.value) async def test__validate_category_val_with_enum_type(self, reset_singleton): storage_client_mock = MagicMock(spec=StorageClientAsync) @@ -3476,7 +3481,8 @@ async def async_mock(return_value): (None, 'group', 5, "For catname category, entry value must be string for optional item group; got "), (None, 'group', True, - "For catname category, entry value must be string for optional item group; got ") + "For catname category, entry value must be string for optional item group; got "), + (None, 'properties', '{"foo": "bar"}', 'For catname category, optional item name properties cannot be updated.') ]) async def test_set_optional_value_entry_bad_update(self, reset_singleton, _type, optional_key_name, new_value_entry, exc_msg): @@ -3497,7 +3503,8 @@ async def async_mock(return_value): storage_value_entry = {'length': '255', 'displayName': category_name, 'rule': 'value * 3 == 6', 'deprecated': 'false', 'readonly': 'true', 'type': 'string', 'order': '4', 'description': 'Test Optional', 'minimum': minimum, 'value': '13', 'maximum': maximum, - 'default': '13', 'validity': 'field X is set', 'mandatory': 'false', 'group': 'Security'} + 'default': '13', 'validity': 'field X is set', 'mandatory': 'false', 'group': 'Security', + 'properties': "{}"} # Changed in version 3.8: patch() now returns an AsyncMock if the target is an async function. if sys.version_info.major == 3 and sys.version_info.minor >= 8: From 28ffc0ec334c89ec35e6a7ebcc87052fe2d33b98 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Mon, 20 Nov 2023 15:48:19 +0530 Subject: [PATCH 19/54] Validation added for bucket type, properties KV pair is must to create category along with unit tests Signed-off-by: ashish-jabble --- python/fledge/common/configuration_manager.py | 5 ++++- .../common/test_configuration_manager.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/python/fledge/common/configuration_manager.py b/python/fledge/common/configuration_manager.py index 0d9ac9ad46..9722619f2c 100644 --- a/python/fledge/common/configuration_manager.py +++ b/python/fledge/common/configuration_manager.py @@ -277,7 +277,6 @@ async def _validate_category_val(self, category_name, category_val, set_value_va def get_entry_val(k): v = [val for name, val in item_val.items() if name == k] return v[0] - for entry_name, entry_val in item_val.copy().items(): if type(entry_name) is not str: raise TypeError('For {} category, entry name {} must be a string for item name {}; got {}' @@ -309,6 +308,10 @@ def get_entry_val(k): raise TypeError('For {} category, entry value must be a string for item name {} and ' 'entry name {}; got {}'.format(category_name, item_name, entry_name, type(entry_val))) + elif 'type' in item_val and entry_val == 'bucket': + if 'properties' not in item_val: + raise KeyError('For {} category, properties KV pair must be required ' + 'for item name {}.'.format(category_name, item_name)) else: if type(entry_val) is not str: raise TypeError('For {} category, entry value must be a string for item name {} and ' diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index 79d910f8df..7714c20dce 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -533,6 +533,24 @@ async def test__validate_category_val_enum_type_bad(self, config, exception_name assert excinfo.type is exception_name assert exception_msg == str(excinfo.value) + @pytest.mark.parametrize("config, is_value", [ + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, True), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "property": "{}"}}, True), + ({"item": {"description": "test description", "type": "string", "default": "A"}, + ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, True), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, False), + ({"item": {"description": "test description", "type": "string", "default": "A", "value": "B"}, + ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, False) + ]) + async def test__validate_category_val_bucket_type_bad(self, config, is_value): + storage_client_mock = MagicMock(spec=StorageClientAsync) + c_mgr = ConfigurationManager(storage_client_mock) + with pytest.raises(Exception) as excinfo: + await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=config, + set_value_val_from_default_val=is_value) + assert excinfo.type is KeyError + assert "'For test category, properties KV pair must be required for item name test_item_name.'" == str(excinfo.value) + @pytest.mark.parametrize("_type, value, from_default_val", [ ("integer", " ", False), ("string", "", False), @@ -554,6 +572,8 @@ async def test__validate_category_val_with_optional_mandatory(self, _type, value c_mgr = ConfigurationManager(storage_client_mock) test_config = {ITEM_NAME: {"description": "test description", "type": _type, "default": value, "mandatory": "true"}} + if _type == "bucket": + test_config[ITEM_NAME]['properties'] = '{"foo": "bar"}' with pytest.raises(Exception) as excinfo: await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=test_config, set_value_val_from_default_val=from_default_val) From 0734b043dbf807cc81ecaf359fde00c1757f542a Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Tue, 21 Nov 2023 15:45:01 +0000 Subject: [PATCH 20/54] FOGL-8256 Don't consider a reading table for reuse unless it has not be used for (#1224) inserts for at least 10 minutes Signed-off-by: Mark Riddoch --- .../common/include/readings_catalogue.h | 33 ++++++++- .../sqlite/common/readings_catalogue.cpp | 71 ++++++++++--------- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/C/plugins/storage/sqlite/common/include/readings_catalogue.h b/C/plugins/storage/sqlite/common/include/readings_catalogue.h index 4562bb066f..9171cb9b3a 100644 --- a/C/plugins/storage/sqlite/common/include/readings_catalogue.h +++ b/C/plugins/storage/sqlite/common/include/readings_catalogue.h @@ -51,6 +51,37 @@ typedef struct } STORAGE_CONFIGURATION; +/** + * Class used to store table references + */ +class TableReference { + public: + TableReference(int dbId, int tableId) : m_dbId(dbId), m_tableId(tableId) + { + m_issued = time(0); + }; + time_t lastIssued() + { + return m_issued; + }; + int getTable() + { + return m_tableId; + }; + int getDatabase() + { + return m_dbId; + }; + void issue() + { + m_issued = time(0); + }; + private: + int m_dbId; + int m_tableId; + time_t m_issued; +}; + /** * Implements the handling of multiples readings tables stored among multiple SQLite databases. * @@ -228,7 +259,7 @@ class ReadingsCatalogue { m_ReadingsGlobalId; // Global row id shared among all the readings table int m_nReadingsAvailable = 0; // Number of readings tables available - std::map > m_AssetReadingCatalogue={ // In memory structure to identify in which database/table an asset is stored + std::map m_AssetReadingCatalogue={ // In memory structure to identify in which database/table an asset is stored // asset_code - reading Table Id, Db Id // {"", ,{1 ,1 }} diff --git a/C/plugins/storage/sqlite/common/readings_catalogue.cpp b/C/plugins/storage/sqlite/common/readings_catalogue.cpp index 5427a27e4f..dcf298145a 100644 --- a/C/plugins/storage/sqlite/common/readings_catalogue.cpp +++ b/C/plugins/storage/sqlite/common/readings_catalogue.cpp @@ -278,15 +278,15 @@ int ReadingsCatalogue::calculateGlobalId (sqlite3 *dbHandle) { for (auto &item : m_AssetReadingCatalogue) { - if (item.second.first != 0) + if (item.second.getTable() != 0) { if (!firstRow) { sql_cmd += " UNION "; } - dbName = generateDbName(item.second.second); - dbReadingsName = generateReadingsName(item.second.second, item.second.first); + dbName = generateDbName(item.second.getDatabase()); + dbReadingsName = generateReadingsName(item.second.getDatabase(), item.second.getTable()); sql_cmd += " SELECT max(id) id FROM " + dbName + "." + dbReadingsName + " "; firstRow = false; @@ -372,15 +372,15 @@ int ReadingsCatalogue::getMinGlobalId (sqlite3 *dbHandle) { for (auto &item : m_AssetReadingCatalogue) { - if (item.second.first != 0) + if (item.second.getTable() != 0) { if (!firstRow) { sql_cmd += " UNION "; } - dbName = generateDbName(item.second.second); - dbReadingsName = generateReadingsName(item.second.second, item.second.first); + dbName = generateDbName(item.second.getDatabase()); + dbReadingsName = generateReadingsName(item.second.getDatabase(), item.second.getTable()); sql_cmd += " SELECT min(id) id FROM " + dbName + "." + dbReadingsName + " "; firstRow = false; @@ -483,8 +483,7 @@ bool ReadingsCatalogue::loadAssetReadingCatalogue() Logger::getLogger()->debug("loadAssetReadingCatalogue - thread '%s' reading Id %d dbId %d asset name '%s' max db Id %d", threadId.str().c_str(), tableId, dbId, asset_name, maxDbID); - auto newItem = make_pair(tableId,dbId); - auto newMapValue = make_pair(asset_name,newItem); + auto newMapValue = make_pair(asset_name,TableReference(dbId, tableId)); m_AssetReadingCatalogue.insert(newMapValue); if (tableId == 0 && dbId > m_maxOverflowUsed) // Overflow { @@ -609,7 +608,7 @@ void ReadingsCatalogue::getAllDbs(vector &dbIdList) for (auto &item : m_AssetReadingCatalogue) { - dbId = item.second.second; + dbId = item.second.getDatabase(); if (dbId > 1) { if (std::find(dbIdList.begin(), dbIdList.end(), dbId) == dbIdList.end() ) @@ -1404,8 +1403,8 @@ int ReadingsCatalogue::calcMaxReadingUsed() for (auto &item : m_AssetReadingCatalogue) { - if (item.second.first > maxReading) - maxReading = item.second.first; + if (item.second.getTable() > maxReading) + maxReading = item.second.getTable(); } return (maxReading); @@ -2040,8 +2039,9 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getReadingReference(Co if (item != m_AssetReadingCatalogue.end()) { //# The asset is already allocated to a table - ref.tableId = item->second.first; - ref.dbId = item->second.second; + ref.tableId = item->second.getTable(); + ref.dbId = item->second.getDatabase(); + item->second.issue(); } else { @@ -2055,8 +2055,9 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getReadingReference(Co auto item = m_AssetReadingCatalogue.find(asset_code); if (item != m_AssetReadingCatalogue.end()) { - ref.tableId = item->second.first; - ref.dbId = item->second.second; + ref.tableId = item->second.getTable(); + ref.dbId = item->second.getDatabase(); + item->second.issue(); } else { @@ -2138,8 +2139,7 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getReadingReference(Co { m_EmptyAssetReadingCatalogue.erase(emptyAsset); m_AssetReadingCatalogue.erase(emptyAsset); - auto newItem = make_pair(ref.tableId, ref.dbId); - auto newMapValue = make_pair(asset_code, newItem); + auto newMapValue = make_pair(asset_code, TableReference(ref.dbId, ref.tableId)); m_AssetReadingCatalogue.insert(newMapValue); } @@ -2183,8 +2183,7 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getReadingReference(Co { // Assign to overflow Logger::getLogger()->info("Assign asset %s to the overflow table", asset_code); - auto newItem = make_pair(0, m_nextOverflow); - auto newMapValue = make_pair(asset_code, newItem); + auto newMapValue = make_pair(asset_code, TableReference(m_nextOverflow, 0)); m_AssetReadingCatalogue.insert(newMapValue); sql_cmd = "INSERT INTO " READINGS_DB ".asset_reading_catalogue (table_id, db_id, asset_code) VALUES ( 0," @@ -2207,6 +2206,7 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getReadingReference(Co m_nextOverflow = 1; } + Logger::getLogger()->debug("Assign: '%s' to %d, %d", asset_code, ref.dbId, ref.tableId); } attachSync->unlock(); } @@ -2244,13 +2244,14 @@ bool ReadingsCatalogue::loadEmptyAssetReadingCatalogue(bool clean) connection->setUsage(usage); #endif dbHandle = connection->getDbHandle(); + time_t issueThreshold = time(0) - 600; // More than 10 minutes since it was last ussed for (auto &item : m_AssetReadingCatalogue) { string asset_name = item.first; // Asset - int tableId = item.second.first; // tableId; - int dbId = item.second.second; // dbId; + int tableId = item.second.getTable(); // tableId; + int dbId = item.second.getDatabase(); // dbId; - if (tableId > 0) + if (tableId > 0 && item.second.lastIssued() < issueThreshold) { sql_cmd = "SELECT COUNT(*) FROM readings_" + to_string(dbId) + ".readings_" + to_string(dbId) + "_" + to_string(tableId) + " ;"; @@ -2304,7 +2305,7 @@ ReadingsCatalogue::tyReadingReference ReadingsCatalogue::getEmptyReadingTableRef } /** - * Retrieve the maximum readings id for the provided database id + * Retrieve the maximum table id for the provided database id * * @param dbId Database id for which the maximum reading id must be retrieved * @return Maximum readings for the requested database id @@ -2316,8 +2317,8 @@ int ReadingsCatalogue::getMaxReadingsId(int dbId) for (auto &item : m_AssetReadingCatalogue) { - if (item.second.second == dbId && item.second.first > maxId) - maxId = item.second.first; + if (item.second.getDatabase() == dbId && item.second.getTable() > maxId) + maxId = item.second.getTable(); } return (maxId); @@ -2372,7 +2373,7 @@ int ReadingsCatalogue::getUsedTablesDbId(int dbId) for (auto &item : m_AssetReadingCatalogue) { - if (item.second.first != 0 && item.second.second == dbId) + if (item.second.getTable() != 0 && item.second.getDatabase() == dbId) count++; } @@ -2423,8 +2424,8 @@ int ReadingsCatalogue::purgeAllReadings(sqlite3 *dbHandle, const char *sqlCmdBa } sqlCmdTmp = sqlCmdBase; - dbName = generateDbName(item.second.second); - dbReadingsName = generateReadingsName(item.second.second, item.second.first); + dbName = generateDbName(item.second.getDatabase()); + dbReadingsName = generateReadingsName(item.second.getDatabase(), item.second.getTable()); StringReplaceAll (sqlCmdTmp, "_assetcode_", item.first); StringReplaceAll (sqlCmdTmp, "_dbname_", dbName); @@ -2510,7 +2511,7 @@ string ReadingsCatalogue::sqlConstructMultiDb(string &sqlCmdBase, vector Date: Wed, 22 Nov 2023 14:54:30 +0530 Subject: [PATCH 21/54] more unit tests added for bucket properties in validate category value scenario; also TODO:FOGL-8281 Signed-off-by: ashish-jabble --- .../common/test_configuration_manager.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index 7714c20dce..c28db2c432 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -533,24 +533,36 @@ async def test__validate_category_val_enum_type_bad(self, config, exception_name assert excinfo.type is exception_name assert exception_msg == str(excinfo.value) - @pytest.mark.parametrize("config, is_value", [ - ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, True), - ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "property": "{}"}}, True), - ({"item": {"description": "test description", "type": "string", "default": "A"}, - ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, True), - ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, False), + @pytest.mark.skip(reason="FOGL-8281") + @pytest.mark.parametrize("config", [ + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "properties": "{}"}}), + ({"item": {"description": "test description", "type": "string", "default": "A"}, + ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}), + ]) + async def test__validate_category_val_bucket_type_good(self, config): + storage_client_mock = MagicMock(spec=StorageClientAsync) + c_mgr = ConfigurationManager(storage_client_mock) + c_return_value = await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=config, + set_value_val_from_default_val=True) + assert isinstance(c_return_value, dict) + + @pytest.mark.parametrize("config", [ + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "property": '{"a": 1}'}}), ({"item": {"description": "test description", "type": "string", "default": "A", "value": "B"}, - ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, False) + ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}) ]) - async def test__validate_category_val_bucket_type_bad(self, config, is_value): + async def test__validate_category_val_bucket_type_bad(self, config): storage_client_mock = MagicMock(spec=StorageClientAsync) c_mgr = ConfigurationManager(storage_client_mock) + msg = "'For test category, properties KV pair must be required for item name {}.'".format(ITEM_NAME) with pytest.raises(Exception) as excinfo: await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=config, - set_value_val_from_default_val=is_value) + set_value_val_from_default_val=False) assert excinfo.type is KeyError - assert "'For test category, properties KV pair must be required for item name test_item_name.'" == str(excinfo.value) - + assert msg == str(excinfo.value) + @pytest.mark.parametrize("_type, value, from_default_val", [ ("integer", " ", False), ("string", "", False), From ce3b1c3076b95d8d118fc9b2729cb508c71893e0 Mon Sep 17 00:00:00 2001 From: pintomax Date: Wed, 22 Nov 2023 18:13:09 +0100 Subject: [PATCH 22/54] FOGL-8263: return raw data for bucket/id request (#1225) FOGL-8263: return raw data for bucket/id request Return raw data in _call_microservice_service_api * octet-stream content type fixes in GET request of proxy extension API Signed-off-by: ashish-jabble --------- Signed-off-by: ashish-jabble Co-authored-by: ashish-jabble --- python/fledge/services/core/proxy.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/python/fledge/services/core/proxy.py b/python/fledge/services/core/proxy.py index 8d4d40b478..ae2e9e8f6b 100644 --- a/python/fledge/services/core/proxy.py +++ b/python/fledge/services/core/proxy.py @@ -140,7 +140,7 @@ async def handler(request: web.Request) -> web.Response: if is_proxy_svc_found and proxy_svc_name is not None: svc, token = await _get_service_record_info_along_with_bearer_token(proxy_svc_name) url = str(request.url).split('fledge/extension/')[1] - status_code, response = await _call_microservice_service_api( + status_code, response, content_type = await _call_microservice_service_api( request, svc._protocol, svc._address, svc._port, url, token) else: msg = "{} route not found.".format(request.rel_url) @@ -149,8 +149,7 @@ async def handler(request: web.Request) -> web.Response: msg = str(ex) raise web.HTTPInternalServerError(reason=msg, body=json.dumps({"message": msg})) else: - return web.json_response(status=status_code, body=response) - + return web.json_response(status=status_code, body=response, content_type=content_type) async def _get_service_record_info_along_with_bearer_token(svc_name): try: @@ -175,8 +174,8 @@ async def _call_microservice_service_api( if request.method == 'GET': async with aiohttp.ClientSession() as session: async with session.get(url, headers=headers) as resp: - message = await resp.text() - response = (resp.status, message) + message = await resp.read() if resp.content_type == 'application/octet-stream' else await resp.text() + response = (resp.status, message, resp.content_type) if resp.status not in range(200, 209): _logger.error("GET Request Error: Http status code: {}, reason: {}, response: {}".format( resp.status, resp.reason, message)) @@ -225,5 +224,9 @@ async def _call_microservice_service_api( except Exception as ex: raise Exception(str(ex)) else: - # Return Tuple - (http statuscode, message) - return response + response_tuples = response + # Default content-type is 'application/json' + if len(response) == 2: + response_tuples = response + ('application/json',) + # Return Tuple - (http statuscode, message, content-type) + return response_tuples From 56754ee165368f0b735e54536935b90f99255b07 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Wed, 22 Nov 2023 18:19:15 +0000 Subject: [PATCH 23/54] FOGL-8287 Remove multiple creations of overflow tables (#1226) Signed-off-by: Mark Riddoch --- C/plugins/storage/sqlite/common/readings_catalogue.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/C/plugins/storage/sqlite/common/readings_catalogue.cpp b/C/plugins/storage/sqlite/common/readings_catalogue.cpp index dcf298145a..54e52f75a7 100644 --- a/C/plugins/storage/sqlite/common/readings_catalogue.cpp +++ b/C/plugins/storage/sqlite/common/readings_catalogue.cpp @@ -719,7 +719,9 @@ char *zErrMsg = NULL; // See if the overflow table exists and if not create it // This is a workaround as the schema update mechanism can't cope // with multiple readings tables - sqlCmd = "select count(*) from " + alias + ".readings_overflow;"; + // NB If this is ever removed we must reinstate the call to + // createReadingsOverflowTable in ReadingsCatalogue::createNewDB() + sqlCmd = "select count(*) from " + alias + ".readings_" + std::to_string(id) + "_overflow;"; rc = SQLExec(dbHandle, sqlCmd.c_str(), &zErrMsg); if (rc != SQLITE_OK) { @@ -1715,7 +1717,9 @@ bool ReadingsCatalogue::createNewDB(sqlite3 *dbHandle, int newDbId, int startId } // Create the overflow table in the new database - createReadingsOverflowTable(dbHandle, newDbId); + // NB We do not need to do this as attachDB will have done it as a side effect + // If that code is ever removed we must reinstate the line below + // createReadingsOverflowTable(dbHandle, newDbId); if (attachAllDb == NEW_DB_DETACH) { From 495e70f297be7a8b2091eff8ae958e8f0ed5d0d1 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Thu, 23 Nov 2023 15:59:47 +0000 Subject: [PATCH 24/54] FOGL-8289 Reinstate both methods for creating the overflow table but add test for table existence to prevent database lock (#1227) * FOGL-8287 Remove multiple creations of overflow tables Signed-off-by: Mark Riddoch * FOGL-8289 Rather than try to fix the path such that we always create the overflow table in the same way doing a minimal change and adding a test to prevent the extra DDL and lock, but still create the table if not already created. Signed-off-by: Mark Riddoch * Resolve second conflict Signed-off-by: Mark Riddoch --------- Signed-off-by: Mark Riddoch --- .../sqlite/common/readings_catalogue.cpp | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/C/plugins/storage/sqlite/common/readings_catalogue.cpp b/C/plugins/storage/sqlite/common/readings_catalogue.cpp index 54e52f75a7..001de85627 100644 --- a/C/plugins/storage/sqlite/common/readings_catalogue.cpp +++ b/C/plugins/storage/sqlite/common/readings_catalogue.cpp @@ -719,14 +719,7 @@ char *zErrMsg = NULL; // See if the overflow table exists and if not create it // This is a workaround as the schema update mechanism can't cope // with multiple readings tables - // NB If this is ever removed we must reinstate the call to - // createReadingsOverflowTable in ReadingsCatalogue::createNewDB() - sqlCmd = "select count(*) from " + alias + ".readings_" + std::to_string(id) + "_overflow;"; - rc = SQLExec(dbHandle, sqlCmd.c_str(), &zErrMsg); - if (rc != SQLITE_OK) - { - createReadingsOverflowTable(dbHandle, id); - } + createReadingsOverflowTable(dbHandle, id); return result; } @@ -1716,10 +1709,8 @@ bool ReadingsCatalogue::createNewDB(sqlite3 *dbHandle, int newDbId, int startId m_nReadingsAvailable = readingsToAllocate; } - // Create the overflow table in the new database - // NB We do not need to do this as attachDB will have done it as a side effect - // If that code is ever removed we must reinstate the line below - // createReadingsOverflowTable(dbHandle, newDbId); + // Create the overflow table in the new database if it was not previosuly created + createReadingsOverflowTable(dbHandle, newDbId); if (attachAllDb == NEW_DB_DETACH) { @@ -1850,6 +1841,15 @@ bool ReadingsCatalogue::createReadingsOverflowTable(sqlite3 *dbHandle, int dbId dbReadingsName = string(READINGS_TABLE) + "_" + to_string(dbId); dbReadingsName.append("_overflow"); + string sqlCmd = "select count(*) from " + dbName + "." + dbReadingsName + ";"; + char *errMsg; + int rc = SQLExec(dbHandle, sqlCmd.c_str(), &errMsg); + if (rc == SQLITE_OK) + { + logger->debug("Overflow table %s already exists, not attempting creation", dbReadingsName.c_str()); + return true; + } + string createReadings = R"( CREATE TABLE IF NOT EXISTS )" + dbName + "." + dbReadingsName + R"( ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1872,7 +1872,7 @@ bool ReadingsCatalogue::createReadingsOverflowTable(sqlite3 *dbHandle, int dbId logger->info(" Creating table '%s' sql cmd '%s'", dbReadingsName.c_str(), createReadings.c_str()); - int rc = SQLExec(dbHandle, createReadings.c_str()); + rc = SQLExec(dbHandle, createReadings.c_str()); if (rc != SQLITE_OK) { raiseError("creating overflow table", sqlite3_errmsg(dbHandle)); From 8773fc74e1533742be2e07d14964b42ffb5b44a4 Mon Sep 17 00:00:00 2001 From: pintomax Date: Fri, 24 Nov 2023 15:57:48 +0100 Subject: [PATCH 25/54] FOGL-8282: now() is the only supported function in postgres plugin (#1228) FOGL-8282: now() is the only supported function in postgres plugin --- C/plugins/storage/postgres/connection.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/C/plugins/storage/postgres/connection.cpp b/C/plugins/storage/postgres/connection.cpp index 837fe36b6d..e9e2cba18e 100644 --- a/C/plugins/storage/postgres/connection.cpp +++ b/C/plugins/storage/postgres/connection.cpp @@ -3412,21 +3412,7 @@ SQLBuffer sql; */ bool Connection::isFunction(const char *str) const { -const char *p; - - p = str + strlen(str) - 1; - // A function would have a closing bracket followed pnly by white space at the end - while (p > str && isspace(*p)) - p--; - if (*p != ')') - return false; - - // We found the closing bracket now check for the opening bracket - while (p > str && *p != '(') - p--; - if (*p == '(') - return true; - return false; + return strcmp(str, "now()") == 0; } /** From 54ddd3ff23045b973dd98b9c4836530b6b068c57 Mon Sep 17 00:00:00 2001 From: Himanshu Vimal <67678828+cyberwalk3r@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:32:06 +0530 Subject: [PATCH 26/54] FOGL-8290: Update configuration manage cache for new config item. (#1229) FOGL-8290: Update configuration manage cache for new config item. Signed-off-by: Himanshu Vimal --- python/fledge/services/core/api/configuration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/fledge/services/core/api/configuration.py b/python/fledge/services/core/api/configuration.py index 6bcc41ff53..4c826ec744 100644 --- a/python/fledge/services/core/api/configuration.py +++ b/python/fledge/services/core/api/configuration.py @@ -426,6 +426,10 @@ async def add_configuration_item(request): result = await storage_client.update_tbl("configuration", payload) response = result['response'] + # update cache with new config item + if category_name in cf_mgr._cacheManager.cache: + cf_mgr._cacheManager.cache[category_name]['value'].update({new_config_item: data}) + # logged audit new config item for category audit = AuditLogger(storage_client) audit_details = {'category': category_name, 'item': new_config_item, 'value': config_item_dict} From 0466acb90cd04ce05e86c3743015cf11521d64da Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Thu, 30 Nov 2023 14:39:39 +0000 Subject: [PATCH 27/54] FOGL-8311 Correct type of plugin parameters in operations (#1231) Signed-off-by: Mark Riddoch --- .../python/python_plugin_interface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/C/services/south-plugin-interfaces/python/python_plugin_interface.cpp b/C/services/south-plugin-interfaces/python/python_plugin_interface.cpp index 59da71af1e..399372d8a3 100755 --- a/C/services/south-plugin-interfaces/python/python_plugin_interface.cpp +++ b/C/services/south-plugin-interfaces/python/python_plugin_interface.cpp @@ -36,7 +36,7 @@ std::vector* plugin_poll_fn(PLUGIN_HANDLE); void plugin_start_fn(PLUGIN_HANDLE handle); void plugin_register_ingest_fn(PLUGIN_HANDLE handle,INGEST_CB2 cb,void * data); bool plugin_write_fn(PLUGIN_HANDLE handle, const std::string& name, const std::string& value); -bool plugin_operation_fn(PLUGIN_HANDLE handle, string operation, int parameterCount, PLUGIN_PARAMETER parameters[]); +bool plugin_operation_fn(PLUGIN_HANDLE handle, string operation, int parameterCount, PLUGIN_PARAMETER *parameters[]); /** @@ -271,7 +271,7 @@ bool plugin_write_fn(PLUGIN_HANDLE handle, const std::string& name, const std::s * @param parameterCount Number of parameters in Parameter list * @param parameters Parameter list */ -bool plugin_operation_fn(PLUGIN_HANDLE handle, string operation, int parameterCount, PLUGIN_PARAMETER parameters[]) +bool plugin_operation_fn(PLUGIN_HANDLE handle, string operation, int parameterCount, PLUGIN_PARAMETER *parameters[]) { bool rv = false; if (!handle) @@ -346,7 +346,7 @@ bool plugin_operation_fn(PLUGIN_HANDLE handle, string operation, int parameterCo PyObject *paramsList = PyList_New(parameterCount); for (int i=0; iname.c_str(), parameters[i]->value.c_str()) ); } // Call Python method passing an object and 2 C-style strings From 8403598d47845f85857c42d6ed22850d670164eb Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Mon, 4 Dec 2023 18:18:03 +0530 Subject: [PATCH 28/54] FOGL-8249: C++ Configuration Item support for model selection Signed-off-by: Amandeep Singh Arora --- C/common/config_category.cpp | 55 +++++++++++++++++++++++++++++- C/common/include/config_category.h | 10 ++++-- 2 files changed, 62 insertions(+), 3 deletions(-) mode change 100644 => 100755 C/common/config_category.cpp mode change 100644 => 100755 C/common/include/config_category.h diff --git a/C/common/config_category.cpp b/C/common/config_category.cpp old mode 100644 new mode 100755 index d2db0482e3..3e7f2a4c3e --- a/C/common/config_category.cpp +++ b/C/common/config_category.cpp @@ -476,6 +476,8 @@ string ConfigCategory::getItemAttribute(const string& itemName, return m_items[i]->m_deprecated; case RULE_ATTR: return m_items[i]->m_rule; + case BUCKET_PROPERTIES_ATTR: + return m_items[i]->m_bucketProperies; default: throw new ConfigItemAttributeNotFound(); } @@ -541,6 +543,9 @@ bool ConfigCategory::setItemAttribute(const string& itemName, case RULE_ATTR: m_items[i]->m_rule = value; return true; + case BUCKET_PROPERTIES_ATTR: + m_items[i]->m_bucketProperies = value; + return true; default: return false; } @@ -1038,6 +1043,10 @@ ConfigCategory::CategoryItem::CategoryItem(const string& name, { m_itemType = CodeItem; } + if (m_type.compare("bucket") == 0) + { + m_itemType = BucketItem; + } if (item.HasMember("deprecated")) { @@ -1083,6 +1092,20 @@ ConfigCategory::CategoryItem::CategoryItem(const string& name, m_rule = ""; } + if (item.HasMember("properties")) + { + m_bucketProperies = item["properties"].GetString(); + } + else + { + m_bucketProperies = ""; + } + + if (m_itemType == BucketItem && m_bucketProperies.empty()) + { + throw new runtime_error("Bucket configuration item is missing the \"properties\" attribute"); + } + if (item.HasMember("options")) { const Value& options = item["options"]; @@ -1095,7 +1118,7 @@ ConfigCategory::CategoryItem::CategoryItem(const string& name, } } - std:string m_typeUpperCase = m_type; + std::string m_typeUpperCase = m_type; for (auto & c: m_typeUpperCase) c = toupper(c); // Item "value" can be an escaped JSON string, so check m_type JSON as well @@ -1377,6 +1400,7 @@ ConfigCategory::CategoryItem::CategoryItem(const CategoryItem& rhs) m_validity = rhs.m_validity; m_group = rhs.m_group; m_rule = rhs.m_rule; + m_bucketProperies = rhs.m_bucketProperies; } /** @@ -1467,6 +1491,11 @@ ostringstream convert; convert << ", \"rule\" : \"" << JSONescape(m_rule) << "\""; } + if (!m_bucketProperies.empty()) + { + convert << ", \"properties\" : \"" << JSONescape(m_bucketProperies) << "\""; + } + if (!m_group.empty()) { convert << ", \"group\" : \"" << m_group << "\""; @@ -1538,6 +1567,11 @@ ostringstream convert; convert << ", \"rule\" : \"" << JSONescape(m_rule) << "\""; } + if (!m_bucketProperies.empty()) + { + convert << ", \"properties\" : \"" << JSONescape(m_bucketProperies) << "\""; + } + if (!m_group.empty()) { convert << ", \"group\" : \"" << m_group << "\""; @@ -1587,6 +1621,25 @@ ostringstream convert; return convert.str(); } +vector>* ConfigCategory::parseBucketItemValue(const string & json) +{ + Document document; + if (document.Parse(json.c_str()).HasParseError()) + { + Logger::getLogger()->error("parseBucketItemValue(): The provided JSON string has a parse error: %s", + GetParseError_En(document.GetParseError())); + return NULL; + } + + vector> *vec = new vector>; + + for (const auto & m : document.GetObject()) + vec->emplace_back(make_pair(m.name.GetString(), m.value.GetString())); + + return vec; +} + + // DefaultConfigCategory constructor DefaultConfigCategory::DefaultConfigCategory(const string& name, const string& json) : ConfigCategory::ConfigCategory(name, json) diff --git a/C/common/include/config_category.h b/C/common/include/config_category.h old mode 100644 new mode 100755 index bc87d943d0..8fc653182b --- a/C/common/include/config_category.h +++ b/C/common/include/config_category.h @@ -64,7 +64,8 @@ class ConfigCategory { DoubleItem, ScriptItem, CategoryType, - CodeItem + CodeItem, + BucketItem }; ConfigCategory(const std::string& name, const std::string& json); @@ -129,13 +130,17 @@ class ConfigCategory { GROUP_ATTR, DISPLAY_NAME_ATTR, DEPRECATED_ATTR, - RULE_ATTR}; + RULE_ATTR, + BUCKET_PROPERTIES_ATTR + }; std::string getItemAttribute(const std::string& itemName, ItemAttribute itemAttribute) const; bool setItemAttribute(const std::string& itemName, ItemAttribute itemAttribute, const std::string& value); + std::vector>* parseBucketItemValue(const std::string &); + protected: class CategoryItem { public: @@ -174,6 +179,7 @@ class ConfigCategory { std::string m_validity; std::string m_group; std::string m_rule; + std::string m_bucketProperies; }; std::vector m_items; std::string m_name; From 3366059feb148ab314dc7279f9641968ad580ec5 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Mon, 4 Dec 2023 18:19:53 +0530 Subject: [PATCH 29/54] Restore file permissions Signed-off-by: Amandeep Singh Arora --- C/common/config_category.cpp | 0 C/common/include/config_category.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 C/common/config_category.cpp mode change 100755 => 100644 C/common/include/config_category.h diff --git a/C/common/config_category.cpp b/C/common/config_category.cpp old mode 100755 new mode 100644 diff --git a/C/common/include/config_category.h b/C/common/include/config_category.h old mode 100755 new mode 100644 From 03efc8f3754a73558e68cd2c7e3dcd44f9eb9406 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Wed, 6 Dec 2023 14:49:13 +0530 Subject: [PATCH 30/54] Adding doxygen header for the new function Signed-off-by: Amandeep Singh Arora --- C/common/config_category.cpp | 6 ++++++ 1 file changed, 6 insertions(+) mode change 100644 => 100755 C/common/config_category.cpp diff --git a/C/common/config_category.cpp b/C/common/config_category.cpp old mode 100644 new mode 100755 index 3e7f2a4c3e..08da8b34d9 --- a/C/common/config_category.cpp +++ b/C/common/config_category.cpp @@ -1621,6 +1621,12 @@ ostringstream convert; return convert.str(); } +/** + * Parse BucketItem value in JSON dict format and return the key value pairs within that + * + * @param json JSON string representing the BucketItem value + * @return Vector with pairs of found key/value string pairs in BucketItem value + */ vector>* ConfigCategory::parseBucketItemValue(const string & json) { Document document; From 0dd3d5b2ff345af7c0dc002ba20321cc2ac41e59 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Wed, 6 Dec 2023 15:06:04 +0530 Subject: [PATCH 31/54] Fixed typo Signed-off-by: Amandeep Singh Arora --- C/common/config_category.cpp | 20 ++++++++++---------- C/common/include/config_category.h | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) mode change 100644 => 100755 C/common/include/config_category.h diff --git a/C/common/config_category.cpp b/C/common/config_category.cpp index 08da8b34d9..ffe63e0353 100755 --- a/C/common/config_category.cpp +++ b/C/common/config_category.cpp @@ -477,7 +477,7 @@ string ConfigCategory::getItemAttribute(const string& itemName, case RULE_ATTR: return m_items[i]->m_rule; case BUCKET_PROPERTIES_ATTR: - return m_items[i]->m_bucketProperies; + return m_items[i]->m_bucketProperties; default: throw new ConfigItemAttributeNotFound(); } @@ -544,7 +544,7 @@ bool ConfigCategory::setItemAttribute(const string& itemName, m_items[i]->m_rule = value; return true; case BUCKET_PROPERTIES_ATTR: - m_items[i]->m_bucketProperies = value; + m_items[i]->m_bucketProperties = value; return true; default: return false; @@ -1094,14 +1094,14 @@ ConfigCategory::CategoryItem::CategoryItem(const string& name, if (item.HasMember("properties")) { - m_bucketProperies = item["properties"].GetString(); + m_bucketProperties = item["properties"].GetString(); } else { - m_bucketProperies = ""; + m_bucketProperties = ""; } - if (m_itemType == BucketItem && m_bucketProperies.empty()) + if (m_itemType == BucketItem && m_bucketProperties.empty()) { throw new runtime_error("Bucket configuration item is missing the \"properties\" attribute"); } @@ -1400,7 +1400,7 @@ ConfigCategory::CategoryItem::CategoryItem(const CategoryItem& rhs) m_validity = rhs.m_validity; m_group = rhs.m_group; m_rule = rhs.m_rule; - m_bucketProperies = rhs.m_bucketProperies; + m_bucketProperties = rhs.m_bucketProperties; } /** @@ -1491,9 +1491,9 @@ ostringstream convert; convert << ", \"rule\" : \"" << JSONescape(m_rule) << "\""; } - if (!m_bucketProperies.empty()) + if (!m_bucketProperties.empty()) { - convert << ", \"properties\" : \"" << JSONescape(m_bucketProperies) << "\""; + convert << ", \"properties\" : \"" << JSONescape(m_bucketProperties) << "\""; } if (!m_group.empty()) @@ -1567,9 +1567,9 @@ ostringstream convert; convert << ", \"rule\" : \"" << JSONescape(m_rule) << "\""; } - if (!m_bucketProperies.empty()) + if (!m_bucketProperties.empty()) { - convert << ", \"properties\" : \"" << JSONescape(m_bucketProperies) << "\""; + convert << ", \"properties\" : \"" << JSONescape(m_bucketProperties) << "\""; } if (!m_group.empty()) diff --git a/C/common/include/config_category.h b/C/common/include/config_category.h old mode 100644 new mode 100755 index 8fc653182b..b95220e653 --- a/C/common/include/config_category.h +++ b/C/common/include/config_category.h @@ -179,7 +179,7 @@ class ConfigCategory { std::string m_validity; std::string m_group; std::string m_rule; - std::string m_bucketProperies; + std::string m_bucketProperties; }; std::vector m_items; std::string m_name; From 6868aea1e3d792d2006acd9cc5cab7b16284e209 Mon Sep 17 00:00:00 2001 From: pintomax Date: Wed, 6 Dec 2023 13:43:25 +0100 Subject: [PATCH 32/54] FOGL-8310: added missing sqlite3_finalize (#1236) FOGL-8310: added missing sqlite3_finalize --- C/plugins/storage/sqlitelb/common/readings.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/C/plugins/storage/sqlitelb/common/readings.cpp b/C/plugins/storage/sqlitelb/common/readings.cpp index 5620dbbf40..b2ce491127 100644 --- a/C/plugins/storage/sqlitelb/common/readings.cpp +++ b/C/plugins/storage/sqlitelb/common/readings.cpp @@ -718,6 +718,13 @@ int Connection::readingStream(ReadingStream **readings, bool commit) raiseError("appendReadings","freeing SQLite in memory structure - error :%s:", sqlite3_errmsg(dbHandle)); } } + if(batch_stmt != NULL) + { + if (sqlite3_finalize(batch_stmt) != SQLITE_OK) + { + raiseError("appendReadings","freeing SQLite in memory batch structure - error :%s:", sqlite3_errmsg(dbHandle)); + } + } #if INSTRUMENT gettimeofday(&t2, NULL); @@ -1119,6 +1126,13 @@ int sleep_time_ms = 0; raiseError("appendReadings","freeing SQLite in memory structure - error :%s:", sqlite3_errmsg(dbHandle)); } } + if(batch_stmt != NULL) + { + if (sqlite3_finalize(batch_stmt) != SQLITE_OK) + { + raiseError("appendReadings","freeing SQLite in memory batch structure - error :%s:", sqlite3_errmsg(dbHandle)); + } + } if (readingsCopy) { From 0cd3d5d2a9b2df207cc35ba90e1aa7ee77068971 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Wed, 6 Dec 2023 14:51:10 +0000 Subject: [PATCH 33/54] FOGL-8280 Add missing control API documentation (#1233) * FOGL-8280 Add missing control API documentation Signed-off-by: Mark Riddoch * Review comments Signed-off-by: Mark Riddoch * 2nd review comments Signed-off-by: Mark Riddoch --------- Signed-off-by: Mark Riddoch --- docs/control.rst | 163 ++++++++++++++++++++++++- docs/images/control/control_api_1.jpg | Bin 0 -> 37316 bytes docs/images/control/control_api_10.jpg | Bin 0 -> 20471 bytes docs/images/control/control_api_2.jpg | Bin 0 -> 65439 bytes docs/images/control/control_api_3.jpg | Bin 0 -> 23074 bytes docs/images/control/control_api_4.jpg | Bin 0 -> 34105 bytes docs/images/control/control_api_5.jpg | Bin 0 -> 10832 bytes docs/images/control/control_api_6.jpg | Bin 0 -> 18704 bytes docs/images/control/control_api_7.jpg | Bin 0 -> 34418 bytes docs/images/control/control_api_8.jpg | Bin 0 -> 41254 bytes docs/images/control/control_api_9.jpg | Bin 0 -> 24809 bytes 11 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 docs/images/control/control_api_1.jpg create mode 100644 docs/images/control/control_api_10.jpg create mode 100644 docs/images/control/control_api_2.jpg create mode 100644 docs/images/control/control_api_3.jpg create mode 100644 docs/images/control/control_api_4.jpg create mode 100644 docs/images/control/control_api_5.jpg create mode 100644 docs/images/control/control_api_6.jpg create mode 100644 docs/images/control/control_api_7.jpg create mode 100644 docs/images/control/control_api_8.jpg create mode 100644 docs/images/control/control_api_9.jpg diff --git a/docs/control.rst b/docs/control.rst index 3d6dc2404f..a0e3be0a1a 100644 --- a/docs/control.rst +++ b/docs/control.rst @@ -23,6 +23,16 @@ .. |pipeline_filter_config| image:: images/control/pipeline_filter_config.jpg .. |pipeline_context_menu| image:: images/control/pipeline_context_menu.jpg .. |pipeline_destination| image:: images/control/pipeline_destination.jpg +.. |control_api_1| image:: images/control/control_api_1.jpg +.. |control_api_2| image:: images/control/control_api_2.jpg +.. |control_api_3| image:: images/control/control_api_3.jpg +.. |control_api_4| image:: images/control/control_api_4.jpg +.. |control_api_5| image:: images/control/control_api_5.jpg +.. |control_api_6| image:: images/control/control_api_6.jpg +.. |control_api_7| image:: images/control/control_api_7.jpg +.. |control_api_8| image:: images/control/control_api_8.jpg +.. |control_api_9| image:: images/control/control_api_9.jpg +.. |control_api_10| image:: images/control/control_api_10.jpg .. Links .. |ExpressionFilter| raw:: html @@ -69,11 +79,9 @@ Set point control may be invoked via a number of paths with Fledge - As a result of a control message flowing from a north side system into a north plugin and being routed onward to the south service. -Currently only the notification method is fully implemented within Fledge. - The use of a notification in the Fledge instance itself provides the fastest response for an edge notification. All the processing for this is done on the edge by Fledge itself. -As with the data ingress and egress features of Fledge it is also possible to build filter pipelines in the control paths in order to alter the behavior and process the data in the control path. Pipelines in the control path as defined between the different end point of control operations and are defined such that the same pipeline can be utilised by multiple control paths. See :ref:`ControlPipelines` +As with the data ingress and egress features of Fledge it is also possible to build filter pipelines in the control paths in order to alter the behavior and process the data in the control path. Pipelines in the control path as defined between the different end point of control operations and are defined such that the same pipeline can be utilized by multiple control paths. See :ref:`ControlPipelines` Edge Based Control ------------------ @@ -323,7 +331,154 @@ The dispatcher can also be instructed to run a local automation script, these ar | |north_map4| | +--------------+ -Note, this is an example and does not mean that all or any plugins will use the exact syntax for mapping described above, the documentation for your particular plugin should be consulted to confirm the mapping implemented by the plugin. +.. note:: + + This is an example and does not mean that all or any plugins will use the exact syntax for mapping described above, the documentation for your particular plugin should be consulted to confirm the mapping implemented by the plugin. + +API Control Invocation +---------------------- + +Fledge allows the administer of the system to extend to REST API of Fledge to encompass custom defined entry point for invoking control operations within the Fledge instance. These configured API Control entry points can be called with a PUT operations to a URL of the form + +.. code-block:: console + + /fledge/control/request/{name} + + +Where *{name}* is a symbolic name that is defined by the user who configures the API request. + +A payload can be passed as a JSON document that may be processed into the request that will be sent to the control dispatcher. This process is discussed below. + +This effectively adds a new entry point to the Fledge public API, calling this entry point will call the control dispatcher to effectively route a control operation from the public API to one or more south services. The definition of the Control API Entry point allows restrictions to be placed on what calls can be made, by whom and with what data. + +Defining API Control Entry Points +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A control entry point has the following attributes + + - The type of control, write or operation + + - The destination for the control. This is the ultimate destination, all control requests will be routed via the control dispatcher. The destination may be one of service, asset, script or broadcast. + + - The operation name if the type is operation. + + - A set of constant key/value pairs that are sent either as the items to be written or as parameters if the type of the entry point is operation. Constants are always passed to the dispatcher call with the values defined here. + + - A set of key/value pairs that define the variables that may be passed into the control entry point in the request. The value given here represents a default value to use if no corresponding key is given in the request made via the public API. + + - A set of usernames for the users that are allowed to make requests to this entry point. If this is empty then no users are permitted to call the API entry point unless anonymous access has been enabled. See below. + + - A flag, with the key anonymous, that states if the entry point is open to all users, including where users are not authenticated with the API. It may take the values “true” or “false”. + + - The anonymous flag is really intended for situations when no user is logged into the system, i.e. authentication is not mandatory. It also serves a double purpose to allow a control API call to be open to all users. It is **not** recommended that this flag is set to “true” in production environments. + + +To define a new control entry point a POST request is made to the URL + +.. code-block:: console + + /fledge/control/manage + + +With a payload such as + +.. code-block:: JSON + + { + "name" : "FocusCamera1", + "description" : "Perform a focus operation on camera 1", + "type" : "operation", + "operation_name" : "focus", + "destination" : "service", + "service" : "camera1", + "constants" : { + "units" : "cm" + }, + "variables" : { + "distance" : "100" + }, + "allow" : [ "john", "fred" ], + "anonymous" : false + } + + +The above will define an API entry point that can be called with a PUT request to the URL + +.. code-block:: console + + /fledge/control/request/FocusCamera1 + +The payload of the request is defined by the set of variables that was created when the entry point was defined. Only keys given as variable names in the definition can be included in the payload of this call. If any variable is omitted from the payload of this call, then the default value that was defined when the entry point was defined will be used as the value of the variable that is passed in the payload to the dispatcher call that will action the request. + +The payload sent to the dispatcher will always contain all of the variables and constants defined in the API entry point. The values for the constants are always from the original definition, whereas the values of the variables can be given in the public API or if omitted the defaults defined when the entry point was defined will be used. + +Alternatively new entry points can be created using the Fledge Graphical User Interface. + +The GUI functionality is accessed via the *API Entry Points* sub-menu of the *Control* menu in the left-hand menu pane. Selecting this option will display a screen that appears as follows. + ++-----------------+ +| |control_api_1| | ++-----------------+ + +Clicking on the *Add* item in the top right corner will allow a new entry point to be defined. + ++-----------------+ +| |control_api_2| | ++-----------------+ + +Following the above example we can add the name of the entry point and select the type of control request we wish to make from the drop down menu. + ++-----------------+ +| |control_api_3| | ++-----------------+ + +We then enter destination, in this case service, by selecting it from the drop down. We can also enter the service name. + ++-----------------+ +| |control_api_4| | ++-----------------+ + +We can add constant and variable parameters to the entry point via the *Parameters* pane of the add entry page + ++-----------------+ +| |control_api_5| | ++-----------------+ + +Clicking on the *+ Add new variable* or *+Add new constant* items will add a pair of entry fields to allow you to enter the name and value for the variable or constant. + ++-----------------+ +| |control_api_6| | ++-----------------+ + +You may delete a variable or constant by clicking on the *x* icon next to the entry. + +The *Execution Access* pane allows control of who may execute the endpoint. Select the *Anonymous* toggle button will allow any user to execute the API. This is not recommended in a production environment. + ++-----------------+ +| |control_api_7| | ++-----------------+ + +The *Allow Users* drop down will provides a means to allow limited users to run the entry point and provides a list of defined users within the system to choose from. + +Finally a textual description of the operation may be given in the *Description* field. + +Clicking on *Save* will save an enable the new API entry point. The new entry point will be displayed on the resultant screen along with any others that have been defined previously. + ++-----------------+ +| |control_api_8| | ++-----------------+ + +Clicking on the three vertical dots will display a menu that allows the details of the entry point to be viewed and updated or to delete the new entry point. + ++-----------------+ +| |control_api_9| | ++-----------------+ + +It is also possible to execute the entry point from the GUI by clicking on the name of the entry point. You will be prompted to enter values for any variables that have been defined. + ++------------------+ +| |control_api_10| | ++------------------+ Control Dispatcher Service ========================== diff --git a/docs/images/control/control_api_1.jpg b/docs/images/control/control_api_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b64892d34b69cd53b69caa632afb9ce85531326a GIT binary patch literal 37316 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 literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_2.jpg b/docs/images/control/control_api_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ebfb92888562512d58023f10bee53bfca8b3576 GIT binary patch literal 65439 zcmeFa2V7Ijwm-gUq7)INBcRe$M5HSq5fKm(5tS}*5ReW^4HAgbI|>2{LXeJ>s1T4E zX(A#bT|x;2=`BG@Anm_7=azHM=e_%X-rs%i_rLG&!0gP-o;}&KvS!V!wZ5}A?JI2( znA3(j-vIyv1KDM#>n9vFREp1JA>@Rib0f7GC-|I&@ z1B`#E54QPhxb&C&Z=ZV~0RS^t!gyL&TrxolW=m z*0bsV+B!peHpAc2>Un$qQU|%(DO+*fu+G8aQif=Y3c6+O;dcw)&UP z-SpqX=HM=!0H9B1x2G!#s8dx19vrsH6D+w4xB(7eS`Y95?0{3iNiZ!BwkHoL?9v##77Tx<-QUY>zX5<; zE_hqSzn8fz0YIY(0I-|1>8Kp4mhyMZ43XP~F2XJBLi1LGdX-LQvw&#!^` zFJbSmVc%|G{ac{hy^4W>3H)c>yJzp;%Kys=+8D@hs=V;(clg&%g7?YDw1;``J{Hyk06iT813e=H6B8rI$8=zh1X3y*L(2d3b(yN_ObBt@e2q_NJ?=GRjrHKI=Ywi zF5kFmY+?$M+0NeKj-!*ai?@%jpFcFp^YRM{i;7>q zsi;I%RllvNZEk68d*A+{<701M|G?nT*I~>gZtC0g%<>L7BLgGzZhz?L{lUn<$;fm>ZV#8H5wq>RgGc2b?&ZGl_{Hm{ePRmN z@jSP^dRTbH6(=PKyIuOVNB^}Bh5S=J`df$o)*l)MU}K;Ie=r7400K~Fj^)GvCm_nQ zDj(QX9@cpTv|pL(t+&}sCWe-fB`8_yz67n7jz${LQoAR&2oqlynd$DaCaJrnb*vi& z3z~!wrO-_j-HFXe?2BkSwG+Z!{asBQaJq7(5*w?(R-Hz7sM^-H;Hw`G`>6X2?g@ zpk$s@SN{dig3&B8ET?2=P%bQtd4xBhAHBO!e6X$7T!q{n|5N~}+=F&S#L<95>P8dr z{nTz%>v%iSxOyIy7;%KnL46A1q{@MJJ@S_I!5)HIB42JGGW_`LT-lO2j}==VH&;P6 zKMzQu*u5lGe@Tlk$V4qy?Ti@e7Lpm(b7IJD#Ak$-%_L&Uq-SXDP~@pf_iv9hE&m33OUK+jd`L<`qy~LUA{lQ6)O3!w2ViQ5uprPuE8<`#d9Me@| zt$KB9>#knD7W~Bd5bbyYfst7+Q)lCmM;2AZEgKV!$HpWv%g)DOD;N~|s90j?+;V>7 zBDpm2i(s(>+^*O%wn5ct<*39HbvFU8NN0gVcQu*6#F@^nhC`gN7>IWAI}vqacdnB% zm5cZ|(An1Ay+Dx$U{tSQo)}0xeH&nya^^PU^#EtycH8Q^7HRveFMEC>S;UZ4DA|oFM4gt+szyqI ztojWH2WPyUMX&malj8LW%5PEqO$IK85>FMjZQM|YAGTPW=IIX}+(#1XqybEf;SkqF zch!YY4O65`NN-|2 z^{ofL%%<9L=i68M4GdelE*BK%Zid8=m59!cCBsfO`;JD)=y2>4jpr+yAa2;okDYqp zQ%3_T;%)_#b!P$^FQv_{RXL)9^uU{te}!)>oRE#cz~g-c>W%U`rzpogDl?$kt=0D^ zS&MR|7mK`tIJFmpEt>uYZ z4adIOTA~3{t8wTG!=@4hy$c4~$GMD(S9I%2yMrI?s0<&zZY5qVAMi%KBDdM%`dcp9k1zOFM6cbV_GKv%4Fd=VFA%qR(>>~W zCS4&1F$VeVCpldXC|;Ln&e0by74Vp_Hy(SG)VkkLywzT5{`1X={<1(ze`hbJ26!2L zmg@7I3=@hX`dd(sHf$0{!V0;--{_PT!!B1>R}$E}!^$1{vr%lXw0$P}j&lPA7Hr~j0#tBNxXsG99_ z^N7#eDn9I4QOqQA^TCTTe$jfy0nsQ(B|N5)pR7xa1nW1abJundhQjL4&Z;VM6tUm_ z*uR~2Z@6>xb`Eint3_p#LUIpj zhGkzza1zJQsh+#OaI{bO=`M5)nM;^m=EHS|(116C&n6!`;BkEhX}4+#Vz*Ko8~UeQqn4I~KfTke0^+u~ z_R&A%-usl^iD7k2?Rp<7P_bVzBWk4#t7ab_`T}$Ju4%&F_DMb0V&|c1KHgMQ&Q6)! zsM(9ZbRA4#<9R?TbY^CL%|nzWaZzLktv91wX(!*Eg$hE+-dGJAaFrfJ_>y; z6?b+wpZK!=we?jR3+`;n#iCM`!@uMuDV!HAeqy8zaQ_=bIPJ@C>xwplRHTm$AJLBn zG+T>TTb8NatV-=&*fVJ#;c#s3UT8>Og7l<5$FS5Cr8-PTW3M5>%adq2c>4_JkvO*rfJR{+sq;31+CChRLr0!I?%nxsu@b|Yg7rdbX zvZ;~nBFdDEMyYm!P%HdEFgq} zWT|c0b?+3SVy1U+*xkd}-?dJLXVcOwB|HzIgf3fOUen+jgxd*Uw_$?YLmk^I;qsDVajk zYq&^npzHbV#s?IH28ZYa8o;tl6+D$oDf*>xKvugu`I{r@@@8OHoeFvft#-#BHsby8 zIfq+L;lXD(>6WCtv{MNk+R`PP`r8O($NntAOciJud=lLudH^cB$x4*SCxi1pKkQ;} zCHml^APv}z!+b_7XR!IFrKLYl(vx{_`N_vY{x)P>?)*!-2~$UW+NS4FqbN+Pr%?i$ z3$9B>FcBFOC8B#jW9>+1YXZ-v-|#cclulO}mcus2edj z$CDcShWqXlyUtGGL{4G@-jm9UjH+iw(f}t)DD?p1KtaRwwuUFh4|5B%Z9d*;Lxy>b zMUdW(eAZPV>66(By6wWOE!UvZt=EW-Ciq6QD-ts{Y6f23+H28%VlRgC9Gbc7v;XG& z{CWyc_(@+Eufk)H(8(TWxU~6df6cY9C&`uw9%99&A90mnGpQ0q<%QiLh8DO)mkRM# zzU^Dz^Gdm(cTy_o$%U@mG_LRD?8$HJiLqWYx-XtuEZgm6B8c!9voSEjBU(hi z&b9_K;=$EB2dk^~X#q9Bc-OWte`mXoN#w- z3Wg8EQA?*z)-TtQ(Um%OW7X1`HJ{}bmdzo1$;==NR!ka3Xz)`?ma+H*XJrc;3=A#( znRMtlFXfqgUDd3|RKhc*xtpdL%Y{pnc*@B5c%ZO)Y(_?T`)p$|iIZ4Py3pUqW&vIN zKr}8{@LEt&W!`i_;S*-2mc57D;j=(h@0IRH?w?}fUuj6cA($=&(SUs_tup@YaQx!{ z(GhETJT?}V_{L)mJt-P8E?TkjT?e)*rg3La*H_Hf*<}~$FVvCu)?^y6eKN4EYJH%Z ze{5yRn{qi}6pctHq3*lE>IkfxcD+<_m?j}DcB9KEsq=mCJ{zV{^LAdrWA&ZY&Z$=9 zq-x6xZu}#e?Pm^sg44sD$`mGNBVf`wl! z%mUtAb_$Oz%d7U6@1B;acUM|kyQM#U0U<8O{3r@%%?SvcGkggx00osk{_~4s=W7jJ z0}YjLw76Ysvb3)opBL6ULtkA~etDzNBJX9t|C3B?iE^0O-Xd+M$}`x=EMbj<_*0K3 zt7(9_qcgXcs5yQ7EWs^(NyxAFU??V2+umrq3%^C(_l%_Ggsu2og7?H#Mdw1>tqG9L zviE3QzfMgIgL|wbze;IE=hA=GN8UfPMD>jBz&u1)y&(lNxit`Cd%2I2M}o(I}KpO5#UXU z5jLzo=nx2J#Iq^S@YJ36neJr^f~F(xN-`>zZ%J1+hf;6PNS*NsPrdlo#5D07e_40B%yD@povyDY@6)nJ7hVMw+jB}@60mBWN;Enzpw=eT<|~Si7=?FZ zXaE*T{g_NW(3!YR65Uz_ms{EU?V!2Pa>Vb;a(|!sS>Y$4e)7YwW98?#_-!csq{B}- z{9YvdJMx3Qd|yev$o{u90D+?ge&0ImV^Rxu1^KOT{?^ z3em(Q4(nTd4U3aef|wp`+wH_z%xBA8&tc1q?|US z>?`Sa{Q&bI!E{2YnSC_if(uYAjJSE#{TrLjf>=G?6f z;ajc)yDGs^RP~?@g}`Ln2ehx}3fAY)fR&*VA_rC5)w2}6BUi+L z%!xS$lv}ooDBxFH5}8~@57b-9RrUG^3B~Jd{t`E%7HIY<>GVyejJbWjA=g*<$%)%H zwU|H(}Na8f0NnM@Q8DDn=R4*rG-*<{enx@A;2i)-3 z7~8tua{D?XvU2+KyRV+mh1Ij@sN$w(cdA7co|P{zZ`yLeG9VnQWPta|eSIhj}2! zcYs}A-0!3GGeX`Q&530TC$p0L=hK%aLJmyv^qI;`A-1`E6L$!p=;^uh;CMU!`#Y-H z=M>j3@Je2;R|}w8Y)VCsEpW}hBPMTG#1BxWb5l`t~wY!NpzS*RcM_&sB{+lalf z#;yhBuk2@s-sz=`1z4G0gIfbTG3;MgZ11t7ogMBRyF z4DmlJH2;Prk~pTFC;sBdeE9rgxa+s7Oj zgQUOPnBYZQMzF&q9HdnmPB#kGvc#j~eBM^7JzE~{`JACq=*;aPAME|AwqpP3<_C~$ z(Lac@Qj#$1CD1k+@BX4kW(uXZRBoXiePPcr{IWQkPe=P;151q>a$Cd=ve*ooIdm+) zFq><0U!(hxXe~R1-4@8D0Z4mL9%nV=)Z4V9*A#SVfc5U8?;LO)HFsk9*8>2j4gA1N zzB`*2xSglo*|TJK4u5?8*ZX4NIy1!|e<4I~!Yr zgWJ#yLxeGY{6)USR98(^s(3epcQO3~Wic9Hg4VP)z!`>ZHj3j@^Cycv1qF~E9v-$e z7q_!j1Q_igr;oJ>Jx)1S=R1#FJn_)-Lc<5)tP#;2RRm~4-2)og&r4%TDZEs$wJG4;jDjSnp9`A^ZE0wa&;*$(!vpVN6fVY5`@QxiOZCMoN8 z$W;hY2isl`#n)_gqg+9Yv1SQKLY6HmsR}es_rw2`r8Y38>YuKg z5dil;*uv(gg16$*`Hqp7-fW?AvD;7|n4-7FKv#-L?jN(+`(OTXTIxej9v%RdWTrqG z5HtcOER@|*GSF}RvM{79Kdj~Hst9Rx>bR4VD@Lyp3}_8;(+&Tq=4={E+@k6@(SXHa z$hs8L3CTxTCCBy9fT1_=Im7eZW%pU?Ztfn=i_!*va1Ps@$NoWN2*K;N$worB^80_m z@2PeyRer28jAY6ykxG`7=Xu?CuI|twpYqK{X<`ZK43YUFKF)&Ufx1QqbCkMy?)W07 zc?or!>0?0msn32(-A`v<&faT-!s8pci8*~!U0wO%HGM~_`W2P%m4nuYY8ze8$q%YI zKo2)WEC}iIUyMGJe*Bt8dcOk8(y12%c=uv1JU-(8OYRwCYqK3d{yq!&jUaDI4%q-7JyZ`V9wE*c zidr=mmUOtP-@P?_$y2aDs4OyB(ONG|i(ZYtYRYBVrMggaN*`2Zh&Gd>)*?d(jqsl{ zaIOa0-}+X##_j|L*geoyIGo_A7Ag?^+~*J%lWgfW+yZVRmUD}b=i>m}Zzy=-a>vcB zw0g?>i6;*E+fxM{)E8z}1uyK_$xejQp3;XBT0%v(&*h)OwyVS|cMNl$hvTlq;67_Ioxj;xQHo9DLoK14lF5W~t?q#$mHXJ#BkXke; z!8;WI>QC1u@oa&`0>hg)LR4en0=1n?-u|A=2SM z*50)lt;g+WmaWdg9+ISp{#U>FazuF(T`x4^si8qQe#hRjRdIKhV6Y%-_9y3AAsSPlx}1 z-ERj)C&ItoNu)L;gVwYAn=u5q2o3nqB}xed%|+{Umxr=TP`igpI@-{`c0BHmRR#s4 zp2~QuGK4WI=G*Y6>H`7?R8r(a7|MeeflG|up%y+y=SMU2RB~931%U zx|v2ngC;U3@@<>AEJ?gCXuQy1u(?n>-55C;+t!bA)hcK!*4sR{Q&M_8EctfH$&6<~ z1e*X?cs2Z;%+aA{)2fPbmutlCX2HaSQtOX&#|${GR7c?SX0{_!4;czxgv4l7$8iA~ zpK{RWG(h`K(-pKMJW)g*9~lON*y0QXAyLZi{*yt57QK?M`%t0W8n9QG&on?<|4?8w zGYjZfubs&Bj1a{ep@*@!?pAibCTpSCTcL&otFb}EB%0IOIca3hVr<~iGMO9ii)uW^uxfEE=K;tYe~g7iHkR}EVLK_qttN!Zg!6Mn@^DSlKWZ%J_o{2X zeEj$Y){e4h3t~cx;o#JVo@4&Y%(t`P%3(}^L{!RX8T2YOT)=!@HP0+lBs!*uC9U{G z&pN#@Is9Rf?fNBSx+9z!LN_1L>4S5tCG~;-Z&R)4?o|7`Y-utG+i{t%-f1G7dUA5T zOj|K-4d6-YNt6CW^{>2T}TH)|M(?;`p&Wjl^cD&8T=dPj^dVzz+V2vwXbq@2`aL;e9I$hT+!RYJ|M`SP!B58%ZtU?^fAZ& zXX2kMAA#C`6u3aX>lb6puLl5xqx{!h8oLwHBnZHzQ`f}HPU*I<#={RF6}J^RqSCt( zPpI`dQ3one^8&t^5zN44Qm@57EOUiW}Px*4-btuY~@(4Zg z#QF^7Im~uKbf0UMFkT~f5*aD@WN0w!m@DP>lx6+R@8Kn<(z|%#Ge2-~C>tpfrtV&f zSWVk%{-cNBuik<`%1J8&uko{>!$=B5DWGRZ3jpx)PDg`FluhBqpK+bcM5vBz(V@_G7n z$BiGokeA~s=rTIC5MyXK<|6arWx|tDjtB{=RPlEigttMR`hPA*R+U;;w8u~X;eX|305j%l8oJyZcY)Mb^?MJ=0Bq-aIC%!f! zi--!thrWq1f|-rg$kDk}FY5*693%E`5ptxB@7bv2t;@Rlmk#bp@r8G-Zi*y*i`c)?*&MKP@ov z$s5si^L@VU78G-$3_h|60Z_T;AbY4HdGonYzaANx#y*8xS3fpb=p9P+_mg;8YhOQG$0Ia0=lrKS1AN{8u0$up9`c5#w$I!=|FF1Go%Fm#cPX7+_f-r z!~c-w>GS0NS!9h_&GoOGIs5&PHkap| zGeC?bpb>M2phGy%k@KwHsQwzmT){5S0XxV41t;}uoap**_{*sNan=3jRrr6D|ATh9 zvCQ))FhUAa)6`}6;KTqzH-2Q|#v?JYZH-6p%u8K_gYCfwYLxF+MNh_CQ$J3ecpZbo z^y-P-EoI3p&7KH3^X>M7bAvHdcxmv7V2n+L)KUf>xk(^6wsm5o#)XD#@tN(h^(y7_ z_kGl4$d}#A#w}ShSX~2*mS<{pE`9sXGjK_LV}}_hO6KL8WPT7V4n^pz90W}+(9KEatslTo?V_qr3qCVdQf2;5&15->Il>phGu0-I;IIB^tj8HL6a+9V<{Va=uWmS)wJ17+u}&Ls^VzbH$3&$q|!?#=kJX*)4R zItp`-%@ec`4VWkp5eIS4%s5bffd<56GlE|VUX_(yt4%OJlagQCUN~+hL(*fr@ka4Y z8!x%F;R2VrR}2P=QXzAfPz8`Wpc0wpIl-D4_>Q+$yP(DwSyg6RC{T7Pg6zDR#eFQ%yOc53V%u8vWA4q3pDCa{#94-Qvz#d7NbuL` zEnnXkKTS<5a#ooZoN2k%yJBBi6Qadw@BZeZ-<#+1kD#9c=3d3Wz^-^0lftap9~@t~ zD`%eE`TZ#9hs@pJrn{?qx8>~3&G+!;M9C@AA`O7ifQ^MQZaTvcAp9aLl)4StMSH&8 zf_H2F6sbg|@x!Wrg`Dc@^0xH*td6jOdGO z)|bPD#%B;m%l|w5@;`#GLn;va@QYMl(5TkU3zHx@Zu*lY1eK_dH<7YkYZ@aAEi(au z$$nkKBQ@S}_NE0!=S3e;>A{J`h3wdXFKp=!LsYiEH<~hxM6+A!rM9%B>qhonh}ksQ zcg>8_2uF=|s$L}IMJUPT*0R6DCEozAG;2fA{m59L!%` zc3S-_Dn0#D@5guY<6@>Cy*_AhwTD;3{E#~(0NfXI9Nf(&OO)b>gFCIpS7oUaW)mu( zRkA~D`cTX22-Xp)7{#VSy+b)4J8y+JS*SS+?_ZV2m{%!CJ=J8qNo)f5)O;dvw3dlY z%|7&zjdIQ-T256~?L66(pTjK#DVT)2O$AInx6+nY{^sM6<=Uu*M5Q@QEn7sfl|W_d-Q zgyGO**&HED5IfNWMDNYaUbs^tGqg=craPnzb`UwQFrRa;$kHq*N8fqk>D7qAi>a_z z!zXIl-(L4m%%mOye@b}`2{Ijiwgoga4iDk-R5{}6IVLLck}qD`%&``kEnseWQwk)N z;cn3{(gn=NCHeNFWspLm`nLS&wThh@p&XJb5MlP6Q7aaO9+9?MU< zOU8*gGpwB@?J2@0VzaGFVw|s9A8G;5DI6pzY8MT_cTyRKUOml@dF=s!&D znufcNcLscO|1Lf@9gv~&&GWqbW@~@RL}=xPLegLkPFK<|Db} zWGg=M2^tUqUq3aEAnxM>cX*!{9^h#PwJ9Xg5p++iJfi^*9>WQW@IM#OjTzmN{qx1T ze=hjh+E2RufxZ40aEQ`pVj?)W@Nvo(;HunBX1(+2>Qau3t6;N;TJ%)-Kr_Ja?)~8Y zA(m90fOcolwO-zZ0$USEP~tdoah5hKkyVG{9e25m^9zN#`xq1 zwi5hNpIgL8w~;|HC#DGBSVHU^{K6z{9a289d-%fq!w9v1;Mu2t(jfh;28ZL=Srny) zNf)H$d#gYQ1ZIkFA}BR}Ln+X0gPshgtAoMnWI0N*x&!GVjEVU8B~<7-o;MU;ER&y- z@GQW+NMEo-=adH4(QBc&^5M72fKl(~Y&NfnTP49CvQE4q-XL9qmY5Jy5?)U>c(;9l z)|nb;4CD%OSvCx2ZU$+5?mhBm3^Q|=?b9cLgmy!YDl``{7MFe`)wRlU) z7uKxn`R>j4T-e;=J>7Ch_p@MpeXnI_?ZtRI@keACARrt~=0;5U)!g&XC8|vLj_1F0 zYTWaM98hYZT*FjTfg0qRrhZ_1#38?;RHb%?R;qT71tp?}nReke2WZ?ndj`%%fIW`(wU-xq8CD{;ME5M=EQ!2}L z1R>n}!YOy}reGn0XIP~u`+JyN02`m_HH=2K=Pz&dW6gUwQ*ojv%&I;kG}_Nw`j;(Y z@rJ9BuNQf13!9L<#N5d+*X;R5{>ExNYOMZvP~P0aydzt@x4XBvA^oMfG~RBuNg)wO z)(0F_u(QZ{YiayOWQ>o%#ZA?edQ+3N{S0mLky^xJo{DQ0Bkj)a!g<;;tuN^OPJJ~> zFn^=BB_&z-2d~6_zDX?k!70d+)Jyoh{Q!ZP%HZ(#5_Jcbm=eT0$^l2KZK~Hr1g}l;~d9$HY*B{>mfvL;HM%QElaeRu_6V_r$e(ZSOhs?bP1) zdzohon-WJtaqVHjg3uTnZp#HMr!Rlwk*`FpmQBnw3LQb#8N}qH>a-+_onBeCdr}|u z@ne_p&tyXMre{|J+oNfK$0|Qm1HuK3H6`A+$9K0pTLPccB(#u`FbrJoQoLSIxhB>Aen*-9P_@BKD3bh_GynI-7O5`o^k2ahf)tplemi1yN^fRScT(P(UDU)2w9Q< zt!sVl>^em^^_C1dl&W8Ns>-Y~DUBEImZTzq_Lb<&@z|!HR90v~;|&u5sAtO_))+IO23FFHELCL@^y_c+A%B1X>30fU#0aHIBkO zDta3ae}yU4R}ta3c#R;#^Fw0&?)m9~J~r}WQh-t zCJpSa64vimN0xf}CNe_Z)vtmcHWp$?cWZcA`V9t6Z;2vS*L$qX3bLzWW*t6{o71;e zS*T4KO!)VqzW8~v1FmzGt}3Nyt`;7a-xwhyef7-%XHL?w_=b9w8z=9*P8hWsy?WSY zBkwV@WX!!=6cg_fotw{SGyYGkR@6nlM-JkE$$II3&nn7 zEPXKqfD}nUd%zPh*cLb)adDzBf-GB}1?%>(71P0xa>Q*=bm{A!q(U5R!{J2q}RR>N(;k_LP=1YuddAC589{(g2VWrs)o0_rTeG(b}cZ6=^Xv#|H#DppOyW9>GfX-y!&lx2E}U>WU&;!bD%ukI_1+S z`1G1q(Tza}W$!g+$A18VXMX1V2AbH+?wc=}sFCk3G0^r&!J-4e&FDNSDect`DgDM~ z&zX=ZZAj%BgHywU45?Ie_?m(L&1=l8AXdQxM7~^TETjSOljv>9M4~>3e%ayKfRMHw zXh4V3n9=z|vSf)`8bD#$K~Tt`P4-(lWKw1bL_nJM(}2}!@b?lQ^3?|JqVcJqxo%y0 z0lu}?NduBV0ApCS4G6op0mVK#)rOpGHNZ``tEpImc=a#HV$jggDNEG@5g9*k>?g(k z@AFuN(s6ZN+-7BOhA!etRl>sfjB|P31A_a}k1u1{lI&B@@ArG4u#PnN6O!s3fq5qb zzOw*X=Pld$T_&4(R{|{xcIUgbpXd6Kt+PMk`u!II4u6wFqVosfLm-f0{fL!%?sU1w zZ0u9`Vjd`#viCRt=zQ|8uK!AjKmt{;B2au7cn7?aOYJ=1 zZdQpO!IS6_3}gRU!zBeT5)XO72VnbC(JzXI_fGF#z&=s zkNM)E>kTS~ynzpAvx?#oV>Dp*34K^B72H~CxIzPFo8YUf;7Z&We2xr!IF}?vNe5no zvj7E4Cd&T$J-hz;-!*5mTjghaKk4_AUw)1cP`vyf%Y);KQ=QgUi;T~yN^MjgzIf)lLMH~MLtF^`bI|7-Xi0#`Mo<1$ALp*# zSfk$jzcHeJ`rr8e_6QxORQwm8f_+cmth?-c!snSyp$m{=>aLnRum9gR@P7nKSAWV;R zjC>Wp8tE>H?w2J?m04O~Msys~o%1VFuAk}AymRL^kFk#4#5Lv)1Ot4c(3+E2f|HG` zWp3q&7?&e@Gq<@qt*A>CFTOHw%q&){!n!8#q<>zSeDQ38sYfc|==pZ`wIQsCD6s;M zY|A>_UoSCf!!>V_Rg!D(!fTTDsOz3{<-~SRE^i`p;!`%$Z@Nn*Bxab6C}S`y*_iTz z>|RdnzDabPv}D(6vEEbvy0Yiw`m(+AZASgX6FhGN$_6lu3XOJW#XY)$1A{*z*c#6) zpizE#~j5#JbfR7tW(OX7G*A@@`qrhv&X=9k$T(OkQRs)eNT}7jL>Q zya{m3*LF{(H5up{q6aELZ?jV}X5Qq>_;{Dd3kF?x_q+RI%*`~o&H`$}ugVfmP!ij^ z!vzr$Ct;oiP=wyw9^bR6n9o${uhxS&M>Uf(`RuPpADuTAPm;6gRtbSSb~$dsLHoNl zjIXEu9NuCO&go*B&Hx>jtv{|@Q9R%C-XUd!HC_^OVqwSC-0#E%Qhhe5ipKyoISx>s zfxi8c=JcUZb>j(PzpWP4>Sck6WvGP}(Q4f_FrKr_y=`o`FZaeF-naa3KhsY{H+M7u z%vE>`f$nhwHemsL#txjb0?JT?HW7G76h@MCOvGt*aBz<;q{AcBb@LY# z=IqOJh0DI(IQNX<@o61Nbvm*cvQ;!1T~XLd;n@60fKhqygZZGkA=}CkB1a5t306+j z=gLRKx{LK+Otv0BFQTRvsT;M8Jgj#n-m1~a<^=UUq(V1daZJA{T+x$%UgHQ}r6t}~ zYjBzQfh~R@tm4+!Je|SCYvYkOyuD(TYEXQd2zfb!;cSk14M+l-QKDCgiie-oN>WJW#v+++KOs&k+C9*>iE!GNO%Ib|KC>KUd`egZn8pfU% zt8*c**$#J-ZBl_$lE@~QMsoL2u}E`*D8;4D+@i$ok|9MAL~s~@X4w*+ z43BM|?D`nM_rFOy|1m7=Ki1Ckus%a)?_%e{0X1~#S5pXEgLL{^$ov7Y;8)wodEt~F z>BoOZUE9S%PA$;n4VphJQ$mphHqniK_?+Ye@rtwbsJ{H$!~d=J?;ina{oZi@AEf_l z_HX9d?&W+OS^&mJ%*VTKM)dgxj(D;-Qzb6Gy3hu~{!;&(hBN)W$Kc5{90@CRRWQ#(^Q8LwhqF-X={6yfVhIk^;(k?Tb0 zIBq_uSo=1`LOgN#NXDl-^k^rl988qxNgz!|pzyA^&D8YnhDU@W#WZP^Hi4YOcw9%F7v7e>dE8OIfe#0MG&6fi;t zIzj>yC^O8_>83^iC4A(4$b20nOSH25Qxy@n{~IoTzBvmY`k*FV!U`8C*$Hh@7l+zGqHlA%!_Ea`G`{7s2)79W7q~6E)ZtD z3g?5-;Sq7-eK13BZ<+YIaq9)HZMk+2JId6m8Rpx>(f9jwKNz(w!0|?7HhKh)GLoB9 z`nj=I!J&6pdY3AZL;hJ#xBJPONE`El>wCv_S;apH=Y(~LJJBCk(++-4Or!2gBk@9C zj%Zi+s~`9EfI%i>MVY>K+LgKZ-H?i0r%(+-^*sHBI`(}R>f+*6xHrRxsS7eSrNluT zmhX>K+l}$=PpZ<>!k6m#l#1kKsy9V4#gj0Yyc4)Ht@4+b4?TYQp^)D*65pk1h%!ONK3v&4b@gGcVQRDs}bD(D$bY$hEqb6-U|^ds-o)xAa6*Eb-Xk^*B(iY2lF%ua-? zU21oEnE<{EZOCc&VMd>WkHLN+8sha^qNh0Gf+c#%$MZT=MFZB|cZK^{vAnQ$qnOQ^cM9AhE(OAx?M#pF<7!q%zT?``TDn8i;UNiiL6areD8a76 z$Rqugua7rYR;T%FRWyAQ`;gS`z&LZ3CsyMH8N?(Ue?UM;^|vJbCAW#p+n$HWnN+eFpUq{#~Q-k>$0-g1l)Gh5$ZhIEJ61% z)%e48hSM5p)y+5BRV%Y8N$zdVeUtO3<(~P`=}q5!tt10)OPiq`a^h!nC>#~phK#r2 zpz;!>CWXfz!A#5MEk!}`-D zA~s(uHo+BRm`w|ny>DHhatzXpAG4V6D0JbgG7!#R3bBlJOxiaU;GTb%b>w3g=ezB@ zd(JUGbr?;MD;OA~axW9MOGeUUDDK+MES@E9PoGj@AXEy^w--+-O9rNb- zEUeQe2A@@W7c8U=s`z1@$7bpL8407WG6Dl$e6u}r*6op8H=D=4f#(Z1?suSah}M{M zr_wE4N~ga$%?(|Pvy;ftpFty=L{Gp@P|6z51b<<;oN@70k57;=naL5C>EK~o=UR}3 zb6s?nDI>_#UC6_%m_Fi*U3qeQ_$spr{lwH&Lk{TGS>v9`KBagQ z!k#y`<6G0?9F{8yhulOC-v6#nRExp4uN%G-tuSqp4X?5pZlwD%bTyPDD9aJm8}Ljg zgE9HzqX%n;m;$3O*?xz8{Y?LGHXXgBK$2TT*vF7GYj_7!@4H719~tnRtW1{?b$X-F zB;4Glo;qpTnd%U*hq>!r`;~j!j;J&xsipNddhWAG&-8gamB=#0?XR_2?K4Iy zDm#v@CKiJxNntZS>PDsT2a9v3DfGml8KZz+IuPK&^07{fVN&2R%@BDmfILZ`4a4VntCXg ziTu2)g$ zG=oN+n1!yDqvmV}#a_PY;8YVfzZt=Q*r$B3qB2PA$`{29D`xG7GHy#R^r1)MY{g=f zSYGD7x&|=xR=UpI!F?MwPSfg(Un&Q|z?c4I?zC6xn$!X;wKkE$R1*BVOcC8+kn|nZ z_Z9P&a$o*^K#Kn?65@v*75EPj75}iEKR`_e{(p@;`G<7+XX*Rr>;CW4`(N&1|9|6e z=9%i{ikHNS_vlL5CedMwkyeYsaF_yte%88naAjS;x(~UMcCk|L#p{?Au8Ip%&TL8i zF*OOi*E*AL*~F?F;;>>l^vH!CcTT>X8Kv8A59GB$gdSg`!ibhHM?BI(mH7WhbjE zM%-icCiE^P=jk80o*(?3I8Rb1AED&Ij(bk5#;F?hySsTzq2HtmMVFQ@t>75NH;0tF zx$&1@L~(G(+`Tk+2`SY}1Aaja8|o7cUXuAW_+1;ston}*YzW16k~%Z5FJ6D}B4&9* z;EpS{@}iyL38OR6<_F>D+W?q0PIhEyvlzb>QwDcT&VgFMqpGk6L1O_O)`kOB9xGD- zs!nB^f!u+gdO8GiPNpu4%{tS)^Yuj)1&>2ew``bJl%zfPe$~f8*q|Wv3vU>!6eO$P z@7twtwYu+xbK14UD;4nNeYZ1wIG>nm=y`o=Gey+Ggh^b~PgJJ9N;P(dmR>&mR#alF zdwDr>^%T;65QEWI2=Mk+>^|3zaNLEhT?*jyIJ14jRb8LN75rJ`SVcn>;!;E3EE;^V zVocVS{=f#i^0rdohp>Z&JOgE)ZnVr?d?9#d?6z3^6FR(TINBMBu>)T?XG>DGhAOmC zxigv?74eDDDB;^!UH!Tep^nOo5`IVMxBrK|_l|37Th~UTsED9Q@1WA8TWHdPg(lKe z2oNDlddEVQKoF#tC5u`{l(yK&z6X`9~5CQ24AOupl)3we%ce~CyXK(Ls?Q`z= zzV%PS7|hH$=9puS@s9U>p67ir-d=Jl-bGAEeOzZjC=>QP7SVvf-u9G{bYP+xCOG~V@#nn5)4bSY|Y#5wBlUV@7;V300 zfPcxW1{FeIAsL z3yp6x2yWRPf)SF=2oly^!O)% zafR(}q88u?XCWj-TJ^i1A;<)?n2)G9duBRe(o)Kzjlw2L_fQ#ScvcX1doS9 z2h#>?z*}I~Q_}NM?*fcY*BQG7#s~X&Ei^t7?@-wgu1ESL61rVFH`tylnK#m3=fl4` zH!((7MiE89D=FZ|TLNJwdzvWQY#*o33pWOayP6-h6!tBS*l@Wd`?%k9l>9tI?!LbYK;A(^cuAr~R1;&3D{%k(##Q!7rw**6#3S zWqW=L!y*}oh=u^}fjT~{gkssMgib|hXJ`LmI#zc4DQ%nvuXtu)KCQaMgTP~(29apC zx{LUSO=7b>Y?!4HFO(AWNncq(w-xST(B*TcO)9U?)m5tp&alKC*s2jZ$={cAC zbsu#4`u2`N7~(Dhvpv3DMAUfW6Kwo`u<+YFS{Dr0UDmcnbgQ5?iQS@ENf? z$ZXNoU4m5?cA=pg+@NIv$gZq6AGaH5db(t8`h40PfXMre&1^7z(XlQGddM4z7D_qp zZ;T(nwmnf}@?p6_be)l~w$EBTxy$}O$?=$oPV0g&QndQ*H&_^z33y~#i*4yULg4#)v7i?yJ@_DJVv=_=rJ>D8V3Fu5Dne$c+bDHjr5w+Uh*^Chg+{ z*NwN^UJ-)Ok3tgy4-VU+-ndW?!$pdT67D2rjm+jL`tp8UpVfe{eFA&y*=9%n9c$6f zjJr|phiG1HTDl9YxZ)3jj~ies2gIP4&@_H{ZpR^r-!6 zY6L*klg&qZy6KU9__`gzHMZE#_i*=Q^h2W65@yo2B0}^rMj5I60&}Msx~`CTbpwE( zR;;&*k>&8DS>Bp}1gybvV(^vXK~Fy2+wal!1NvFAtr96L{tjM_1t%@~CM{Zo9=IE0 zj+OgkX8-|fEMwE|7Cq|dqJT|O(>W*hJ$M?tUrD+^=%J--^jfQxU*f&PQWu< zsfjOf)BN^_X(3cy-D>3-DCRR}>FVoAX}`A1Jo+j{&=6`aJTS?QwV6*H2%qpT0M3O|0F%N#w)% z-?A7k{~*7n42Z^=IYHvamb@!mWVW_UGit(CHe03E*k!dp^*(uqGuD#KIEJ2K91EKX zYoafb2R8JNl*jKlDnNFIH+6w^QWWEB@o44pOK?ij7|r?FF%@t-U%0ElLXYgpc1X{O8gP#|L8;K37+9 zx^eVfFFrZuYYW%J>XjhX#B7WovX0{z!mk7sARnL<&NUkqoFyIIc#~WnN75^*?8`>I z9tg`-6#e`SU`hr>o<*sA{SWGR{~191AJzXI{PTYR3;JgQ6n~|Q{Ub~DzhwvT`yIjG z%73^s0QhVId-R4hg?lESJY($pE37l<{wCV>&wT&L#v3X75sI5}?0XWJ{C~>?^Cx12 z4nF@5wZ&1@0GY*i03h@0l^Wl$LlC)uXf+?A?J1;{k|8ZjXt&yZ}f(Ku~K2+$waaQ1n7AVjAE8%sSBIXI8_AdT_aYkm@*<0tWcST4a%WfQ^*4 zx(^CS-v>niipY_32&^1_guLTU+S&(^^#t(AL}#jBC1npm6$w?B06e6kOEVDfMO9OC()?)31?$2Zr) zE(vm}Dj6Qhelc4iSXis^ku5*RD6-S^PQV%g?L8k%84e$RnWFyr-3(kf`z>a2y?3P^{p~VjiicgCMZt@)qM`^h{AYor*1X(!FJX=zeQJb~>z{ z^TSBMX}Q)z^<4F(iP}fvg?Qc#jQHx>D(ZA?#egfd-&J~Q`riB6#0w1f+qtwSXxy%p9oP%e zfXmDx#x-4)n*sE7-Epi8onE0cn7vM-$R}R6QsLRSkoy~R%pIw+*mg}j1*af=Hu<>A zY`fn`=K{vjTmg!J99t=*MFu0^cDwIeIv;VatkyY(=4KFP;P$}*qkB6Q%PfdY)3S%Xus->N`;DL;i6}`E7~H>@k_5H`^N``S$W>+^=j+LS%5eKt zg`V)$Qy}kTEH2!-7P5d^DJI#$wJB+6J5-c8jLa%w>5pgy{I$8dF<#Vuj8KZv`yp4{ zhxZPdPlPmHX8Xw=W7BQw;7tZmk~7ir*%Wl01-|WL{dr;-$&4&Wpl$M(X{;9j%*0sb z*K+kmuHHuooMkM~lh0sDHbyQBsbdiFH=#Yivphh6G)^BNcFlrUAL5f>M8`Ki22E|v z5kB^nL#}Q`P2tq8h3`LGw=r^Cp!K?%a8sRjQI(Cj4+5A`4VLu+4tucJ5gf;p+}`r^ zLqs(L-ghb4P8CWdOUH8^U#ghR+HdlN&)Jv>#vgmVBT;fhuquUm%;-K=U@W~AQ8vCZuPUwT`&I-tA1J^4_Id@O5Ynq%+C2z@0et<6T<5~Y;X@x>{2ev7vs(!QiA>?pTW=g3w{PN`Ts%J z?$ti1RAHrwSN_;Rk zQ*R&*Er`nYdzrRuPyeOtBu(OZmP&UhR$vr?n?lX-L1)%i1qsOrE+3`!hMg&fih;6K zZYjRBDe3{29q*Fe9k4@cnpbh`1NK2G50e5eIG88Vqabrx7I(5ySX{4>+Gx?c8xu6h0Qqq zV6E;nQDh%94z^2m-}VL;zb^Ofwq|eQW6EnvDi!5bmE1F?f+~9$Q?Hd$hd|ilA&Y<{ zDq&|eiX;fcekvegaAEvH+oL#*8H!-D*Q2%CtPdy*%blQ)${My3zTwO)mtxH>#JMv) z`FJRKSF{z3*J2#Y-)RnztR(6luWVYU_1S4CAwrt@1$ql_9$k~Ru7(!yT^EJij-Bt@ zdHmvz(tBDSjey!db%B>P=oKjf3g_MA&JCBK6bHz3<17SQ_yxERmsiAOwYIiC8NdD+^;I`cavPQnIPsQp{7M zVa!pxC%e5DEZ4Q$UD}QX0>coug^2`_KLXlOv~XKIA1Y$qohtZMDWEdnLF-ySiYznZ zDxpmhsp-}w@{o80ps-;`Qhi?BLEOT9Fa+MZv~$$TUFM( zNQ5++)VPHmTkKZ}b=*pcifVtCQ^PFl%`6*!L(3(=?7;H3)<_VMc3Y4V*0%BtMI^Ox zhZekA1EZMfHzJ+GT9szHI&&HdFXV_VUzrjnF5-0?(oP#3B{sDx@y8eF zNXjqU`sP6!?%7r!Y7A1iH~!|>b?FNqc|9^1Z)esmH={YI%tV=%Wcxt)-PssHoe|HH zBdNKTmQ9Bz$3z`YHm>mN`|PrAwis3er`GIgWrC~J5@N&H(W(QcEjenh-71kD;c=WM z%%o3`g<>KPPp}Q*4jIzs8{H8?um*7H=3Fh(n3a+$T$5v53D4!Y&zWwxc|*X~WHK0> z%n)F>yAxB@!?v(aG7jkKYw=emYvW6Dk{ptDqCKP3rB?Z#ILo3x?4FUn5(dP(K@ZjW zcQkz6BkC2&ta!+ivVD*Mv8$C^V)6>TpKiLM9a!AnGiE0i;&_QqA_sVF zHa|{d7y%&P2jv8#^NZE2hrR?n}s3-W0^ngaotED$Rs%x4*AO@y@CW7k7e z#0rT`rlFmDSC*!=<&C1C&S7z9Ew$=v%8)xqhSzLdDLXFr0O8C#lpK|-GZg-X7E3VE zVz!(1>OiJ*6y^G?1-FB%6pky-k$L2HvemP^iK#pKSE6x|puL>`j7ITi)c+r(TL%JO zvD{nGwcaDHBNKA*z|(KH7TD@QCl8nZt!z&nQ`j^IoZA3Onao1TMgJl&E(*lPLkI)2 zMU_#vJ|Yn(o@M#)=*!D{Z|b>XgK848M=;Ud+eQ!bYby>4g+F_Hzwt4+1vT1+vtOmb zA~E)}9LRcZbB$TR-Hm3b1LqPSU}k4qeP55bmXVrN3vc;;djCR3=!tK3zr1+iyM_l~ zUxy4vz^LAiqae|a0vuCh7kmh+$rSF4AXIAiy2>Khd;G1riB^6RM+?7jSb7wA6gb}J znN}Y*`fRClbQhYu7$6+bfMA!Krhx|x13;Lg<4gSV1cl~l2A{CTy{rzqq|#AG`b@Vf zo#^0|dHKq^(_C4I59QjPlcY*uNjeT!B;J>a2G8X4ix9=ImsaIqp#evRds+%`&KEva zSV{~P@!YUbvbAKsEN_Mf~kV z4tP6G{NS?>;<-%*V+z}yl%%q0hVezqBfyKk;d%?pFmFu;)buzVq|!f^MAFJ?#d}OKPmtlUX2VeP#DCg%wh5@n_W!;*WkM z3DL}>=@A_>#ARyp`Z2_!Tdme=%05U0NB~A{UIo^0?VNWbZTp~uzy+@lV+k_hw~WdM zGlEw#*L8;6%OWtKmM0H%tW}Pmkznq8r+rfgWHWMdx&+cN9<5Q(HLH4a{M3h>?AaJ6 zYQJHwvTbG6hd3@stWn?dJww|5!JqKE{yKa8#PMv8|D7^axlGe6umoVje^pj759IqE zH!F1huA&9f`CGW{pZWf|{L6o$gw`L-62Hv~!!@MP5|8O!24MO9bv-uzbvfZJPkpU!hfHp-Bn~mq8{(-BUq6Y(&en{pf5jZ3|BA3qAflZa(EgO}RI#Zo&YQzt@SQi;t zZ&d?Q$?56TX>S#W5wCdyvR>SBVA75)FoTx;rUiUP36S+V&YeyneV6qz2Vx9@@17y9PIRLMv77UQ}%K9$r0{Y^4h5C!P+ z^t}a~4Ye{hAvUBy-nSw2z_b>N-UnUP%mreJE9hNz62EictHd5hnk2KC&uz zXN~&+ZrJm{6cjtxE{`wE2hHR2=2AO!uBBc(c!X4rjA)Yj&72Tp@oHNYD*Yq zOV8MK$}wKgbn0ZW!dk>-3n<*3xDHRi0(M#;x?z)a402BVjt&z+R?^7f6BkmA7nO1Scl&?DFF_a}X6+hTPqG zoe?GO&D{dm!e(>O{#jowxhj);nhIuXV?#wFN79v*wKkL!B#wChBSbZE!y>16ASif{D?}IcaN6g-44<@@ddx`PJ zbH31wM-AJ$>%ZHP&-I@sdUK z7oK*OiZg>Thc(-x0QCr&rnDmjj+m{kcDZo!MOVq4duDpPabh95E{dYgy!Uw6Ha4ZNdoD8g#7u+df|x;^Zdi1NmAq|=1MOGW>Bu^n zyG7$fJJQYa;F~oTcypEZ()O$xP5oKL6|op#cS9!{4#qP3-|Tg&Rp}|_ieKdceO{MQ zE{-L(O3Y64z?li33kX-6nGKGheDBU4hv^Txt`!RR&sJ9IYOfB`rOk8HMK|M1PgPD7 za#}lc$bIE!!9&99RA*~bY0dGsA62H$>VB|Qxhyoi%zX8}pbn2e?%_gXwA9cc=&`)r z#$MWLvJ&w6!Xh!ot;I6{!I>x}om-aF>1kC0ZSUg^5uLLNdAeoObmI6gI)sA8KC;LM zVyo*Kx>WLTUz}9LLVC${d_70Nl-Y^t7xw{?Q{@?hP`T;D4mBg8a+BxATt?)eRnj!q z`tFQRPw746Ii)FCds97{?{<)4`+OacN1*}C#(}jgB3-(SeLJUjWn)9Jyy3o!Bzx=j z>cS@1jm#Nc-UqJd!?mRqUF!lFi$w|EPgdbqIi9$ADr)woDB;V&~9T}+;&D3}GJZ{;z zz2{tN{V=HKpxz3d{T?sHCmIwFR0Q>@Ut2h{0K1wy9(7Efa|gsKfIq&|5j!@ zxGP)B`oT!V1vi+;;SW!{=Z|>2$bj%f$q%*DVjhqUh>k~+L@E`+J(^2GuSM!~Ph1i5 zWxXKgI)2&q)Z>W77$Mi{u>EmUpqAezgiQF+C zUK#bc{}RPlre6?KoJr27%_L`40ufI2e}LWidmP6f{Y}$ZFGY4Hy5eQjqgueM?r#P> z7Hv*Og_NhwvGu689{n^<0|S}+0E!=^J5yP9?a*a*h2AbXx+%(q zEL@no6#;P&vVY}K1SXmb0DHEtRs&$XvFWWQ+gPyVhZpJ0LcJbvmKFHZ3$BUjrdXT}KyDtlNHc#pHJ zhfE&@1k|&BSAMq?3Bn?<4sf77ucL14gFaTFiTU8)SFGgJlvgUIX&5A=&!2)9M5kx8 z_hhSGdi}hlxgnCTDEJjGP4HraKnUnQ3}2?k1#F^Pau73jU}R%~-<7?MdAeAk*P=P@ zW2$kYy@s?b`yl$O*mMemxjpr8%RZ>Z==T*6Pop-;n~6;4T*Dk2zWSe~iTDqF_6NOK zlNqQ8``sfz{;ph`AFE>(;{B3P1o47w z!r!H%|Lgnqin{OC>z{3L!{_si<^*Fv!kLnq(5p`$F{$%{$x@6!Hk|X0=|m2WeGr@0 z@5&ZKn*GhO#kHWG#votCE%Kk)0ddBVhxZQ4E9e;6gom1df|Qt6)p>v4fZvz@CW75d zv_EDn$M+G0+la;=D~x|r-u_27_nV;hk3aq2bFOg>$y===kAt&3#2*~j`K~nn zkMDP3lYEQl%`3TG)15jIsb=b$qm%uRGI$Nh;rsE}5MJb^cP3Flvu}OGW;;h=pNOYG>Wtu63rAMZ9bvG{F(xf2>(-Mc=EpDHfs znkgIN_d^uRr*NIbXcrf@x_0NkiQ?gW&bjso=Z4S&eBvG8Bg9%59vmt=eUkX#zP&gR zsu$!u!1>(F{EoBh@$TXNWBZ`2kLxm@jvasb%WWphmk0uG)(x3?XGx8j03zr=_v|3c zy;+*ltCd>qraZZ6*4g9EePx!1j7bbJxme3Ue_C$uvNni@Ch#oHyv$f_8&#DUF4u+R z>_JnRcBRPR3r7>>;%~pdy1o45BdrbUOn&sLkwE7W{Qv{JLNr+yn@+dPh%H!E)~YJ& zMY)|Bl(CLEBwXIrY_<8~jrik$wQoLI?x{~>hW1hcH@l0E!K_Fz4#o|aB}l)XrCjF` z9+vYLHxp6v9$U{fO~{P;ToFG0S>TAQ1H9zkZSl?`%WjeeaT7-yQU}c;3SvF`H4r#@ zF{!d(NiiXly^!Z`B@3Hl;a&321KS)nd>T|Jfo!)A(||l!%MR@7;m}ds>i_}?(-ZT~ z_(;O6z!9Id1-(dThnAM|qE}w*?U{N(>O%J=3l-qwBPwSOlW4aCB+Q0)8b|u=p_T5UYJTpao6y_3R^gFL7bOY1 zkQ*D7@m1$ALreTZLu5-8UuXPs^WLYu-aZxktn%z{&4wEH1nWu?I?n~2%~a-HKQT~EpOQuuBwr(jp{5-z&sH7sQ5y2lyHhA0w0tMlghl&YM&>RZLtj4j zy2S}K|Zqa31li!-I*A&vxTm2@-8j$B6UP?qr;PZPid2tPJ|$Gt!s0ukc=>;gz`NNQP%sHQwH~CRbw` zUlJI%!w8lUq#L6?JWY>kDQT*H`h6&mlwf;qP8q3T%{0Xd)s;ZmsEE4u*Aprq3N;PB z8iF1i=H!};b)eaR=~8m)MHCvx$k0sUm>k8Id~ueCqNUG1oJHc+Q=;{Q#yo2-5wS2j zS|uItKFN9PyoDE_jn*IXpY`g*_n)>!{=w@~Qbkg#)jk%iFU`5KMp7|Ht*j1tal z!f35<+F;Ei-qy(=@?)Yif#)_X81HlOD#3iY%QwR(Le7Fg1Eh3Ko^a!rlpP0AUABy4|<9}?DshpNCMU9gQGGZ*Sh01&=#W=b^^KWci&A)xWa_? z6BFI1w-PoAFIe+1x4?4#z3Qr3*Udr1QrxG?Z2T3s;X5)xIGM%(A9>sb|aG&H5&!S$Wx zMPBC`PS-CNtq^iy30)_(QG=MXG;^r2u33dh_^lb#NoPxuAy@KM`y7~(Da@?<@Jc+i zs7}92aIEEcZF^jj(U%M!ueZBRkT(94lsq*FpUeSBDSjcMevGeKjkT(*{)OYt1&mI? zQkVThy4e2ntUe)0I~P<+S`99E$n6b-j6yhBL9BN*&(h3D!DdmhWI2ET8892MQBUSZ zf1RZV6wNxJVb9xVuJ-Im*3qYBXFeLNtp($H8{YuJ0j#YJ2b zm3dIE%j`@^rP$l@gt;#!mCSV3E?2!>4?QSpnWNyXZTJv++J}DG%FbYnrfoq)C-J&pzV2b2UT#0xQ`1eBRzncD|nBG}Req-36(yuG@ zB9dVxR?bU|*(Onf)9{lwZ<=#q2IG@?Vh2u^*o-+zV2;s)>nC3)YIOuly^(d&Tc037 zj^3c&i*Nt9vAs55Wg)u#ir^j&SIzpGEE6gFg&<_fOkf)>k!Ct(x|HmPX=AZ?quqTp zCQYxS%=UrTdvl8L$IlL+6Tw9wcpvEk{CXWRc!pxRY&5tgm)rmCNQJ@!xzt9*4XgN* z6OKzy@0^-$I-2DP7L5R*ww~CXllnq%|RYS#c`|FlRXR7Uj zIzUk?!~HLlr+h`8G5C13UZ&WA&NfCL1JUn|OTt?C05|ZnLUZTj z0bg5CD{-9jNWpuj-l7We7i)QYzYp3B#;zC^TvAls z`COkvUGJcad;Z@NBB&kxxZ==x-`o#zqF|du;fv}JtJRXkOID{}6s%o_y=8qtdnh(l z%;Ddi8b8{c{}U$YpGdj+lT1(^!qrx)`~ch)iw@&Im}F2~H)2&^<2u!*xfTfF zTjZO&u3hAfKUusRd9>}TdlZ>&1{tp8@d%9caOQLC7dzB(m{R9`zA*AA1G~nllXP5d zS0j5;uv`ORdF5Rc%4#r_gv+++&qpywzEV6`k@(Yi?Pvo_NB`6P%!5+eXUEbgoRm+v~rw5 z{V_9180 zYA%*Mlg;zoqDLpJ!dhiwfekeoibu96<%DfB(l@;!BCgkbs7OuIT?a#=)lOB1-i|%b z?j?A1jzzmLBclOTBL{dF1nBD^066OxpRA@wmRVNPHgzP`?pHTkwj-is<5p}XK6IV8 zYc(zIF-JCokJhvLRP2r4B0xfAQ}t&%1xle^p0*4 z&dwyYOhWPBuLpV?(s(f^3oQICil#{^7OW&awFw5(_EwYcaPFMbh&2Hmik?7 z<7VKWM(+L_`TKv?r}uMgpI;#?|33Zv-Vgg5wkjKBH$fRNGY2C35v_>Dc!XS$-{Xy8 z)nLg-CF{5c7mz4g0E}Y%&j3LFRmYer5kH+ni~!!PjO3}p;T0^7g3;Qz)hc*}8zpygKzaKA(Wqu+vK+cY;u z@wP6L(~Zrb@dTC5QyfRRO|u3vn=kq%ze<>NCAMgK4D2wbTEkD1b!%iJS?y59a9O}V*89}G)C-$)Fl8}3;;rh z8wAS!e_uY(w@d%^>;^|K1YqXV8ShX)00?0Nz|$dt#qEc(A+6RInT)J<*yTG5WMRnk z)YW+}B=M#8ZAA~&ZfB1V7VTfM9p}G$e{dn8#=3lSSZxhmgoC^EHHS~*dm})Tb`ZvbC1JhJMMu_~VXfZ&<;;Ip~v$~|JgX>cG9_*s=oyAkfEbfSU1-sFZ*I|DT~%{tS#9#RsG~@+$&VyVJ=J30IpO zS(A~WWYxDzi3LiD;obJGLV0|*-%3<#std->eu%6K-hM(v6p~Gev^Zo)8ebfzCnGZv zakIb3rds%}4oWJGRod7r=*Zow{5V>5K>y>*dKlOxeY|#N`Xf3NQ9iPI3eGW$3Qbdh zov1IVIUX_+*>;DqRNIW~evOa@~;7)ohr(NDr zHc4w*iQg`rb@snt1_$RjRcgw`4$p7F!$~=UP(rWMG|+K1d0__hX{}v6hNR* z-cB8aGiH!4;!oj;LxT90mhJ4f&Tp}3;nf-M+Vb*9jF@PUXp)oms71b}^QSsJz;_u* zP(g6kAt*6eBn?0Sg)yr*%S5}CgPEq6>HTw4#;nqo7+1P9b)h9fSB5{kxo&@PH^BfR z4WhM_dNsy2Lfk%Rh6*rjQXm!_=9T!Vf_IBG*DB5BQw#3A(N7#<$uZM5%iQGEw2JcC z6+wxol%Hq;x>#N2Jt&E|)KDrGTCVawf3?gIdz;BmoEUQJ5?E1J0QCoS# zK;i{6Yt7)EWDf&L!DK| zgK*uMeNeb1i#s#MC$@34gGEcSqJW`YH;*K)1>U=wFv;U;@xc)d7)Uc2K*j1-}MT$xMOA%L4<99jCkI$)ppD5tO9JFTjwC&XSRV=;4 z+0zlY4BlSaawAJ&?0QU%K)5H9v)%h{UV?$t`-*CvHD2guScv*r(+9z#y)y)e2P*QG z(<8?1G802R$r%EL2{0ci%D`H+R&QoWXEN%zizbAMbO8=U!OVU2L z_R9esUYayhkRKl#j-4Mo;#u@))Xcn%{iRxx)L_b+W%#j?x@|K-{;lFwE&mbI8e1MAfAkuM9?~%)09O)6;E*-7b4}l2YF| zw??Wr>)quIQ;s6tC%Zzx=h|NLscQcDUd6w87ysz{%NqKN?|VSAgC0%ca<{O3;0@bQ z8AXhQOjDyPGftHJEN8S1)`s4=s0ECO4gk5P>ZkEzW>iqNbFVSx*ALMCpGgmR2WqhT|NucYpK@MIydl^S;mRvUx(yE99(X|W-ty9vnZ;ApfPlIg=tqN zSgKRj+?$wnZjL|D0o_MlMN-Tuht&cV{Y5|GzpnIRmK4y|bZ4$Qe=t$JmOddKx!{gp zvl|~Tx07k&lS`a;9w*gCbsLc)b2d`4s8;b3FC@E9o!2ug_?NP} zvO%|XH`6dr#DyNt*80<86f%m!!-TX&dAVskq_2>SxYQqSDljMxvbCHZFDDt?yq| zI820O`O@7zJ~?3$bBKLMeyO8mPU^Y%?n}Lo2?wBe^_qe$0ldlm4Wj7v#7j6w;g;6d z>~BT|BkrD0pqV3~Yq|IyZ7z=f1l{jv>4ATAJX^;kqJ0w(ShD0m>|L0u>HKkAs`J_Y zY@V|p$K8Kd_I1!dH4a(#Y!FK$U&Xe5omT&ri0{Aq8zKr&CIq{~C_WYd9JcFG7J+8= zNDO{mGs~#w(>|qXbF;~C@Jg&`czHUD zPY`sS$*FfK`N>TulO3bcR?SpvSt%!^y`!=QkA-k84e!9K#2OaqQF%%th%xK$jq!ix z@1HT)|4bBI{-cnX`rO=}9E<3d`#jfX?r&5zP{ik04|K|e$!QoCyo3F6PYsHop4SDq zhueVRdAVzyG!M{gq9I)YY?NHMw{<2l*pk>iXTvEYf=8lq`aBd-@@yBG+RSO{8se9#`pV5ic}4TPftq(cr~2}XZ@sS&pWIoc$8wQ5vWf9} z9EOZ1O_w-K^n0^jggns`e97|3&Xc-%7ZaO%pFJWQ>SUgilh~JCsPoW$ zp=Dz?N7FZ8sfD?{;XNa#bs@qL%wX)b(fZUH490y}RrY4gYBvvwmMn-tQD|k*>nZNk^IAWY zEFt)-v#sR>EptL4cSXAV^E83s(*rYQH8IxJPk88cCT&O8qTwAdXF*o6N=Ly%q;Gd@ z-n@%ta69&n8TG!1U@l{@uAvifoEP=OX@9(kg)y7f=}y2YsG1J|vmcPCyFksvwb`%6>dF$$A@Ag0OQM=FWkm zFCs?m%;0sl_iNo09wM*y*KxGKO57gLGDhvBvtW^zpLsfWNi0vVN*z0Xg(6&{oq2G0 zN$cQYAFTRS4Uyq4kzo~oe+_$a!^E%O-gB)fAF94Lbjdh*%w+;<{*HjmE>ja8sy{%! z(W|0^)din$W`YrK=@4v^`$y?7vvt>bBOK!{3pd>`G0lv#61)}yJNy9Zei2|xmE=a< zjcvO!r~+lE5s7dwGR%XDuQd;6RE(2gl0K0=c!xo)Nyf|0JS*OOaN+45?fz|ne6A`(m4 ziTF0nzHGGo2pQ?A_)RR02Ugrdr>pB{#Oq-DeB~erBB_6ZPRLEIM}zH+Rc0YlkRz%f z;_a%ls!L*ou+8(EIzkC?y4L_h-00IES>ykIs$-j_ssh>&$>6Id>VqJrbzvVA z^F7dT7w~oV@Jy2r0G2V%co5i+SCU1z4akHK0IiNEohk>UA^7{Eh3o?@Su0c13zb(c zvF?H9(AabRh=rJSDrf>od;pN}ekd+EWSoFfS&oZReSjn+Gsgj`RPG#bjI83*4yDO! zQA1x!Q<=2h+C_s@d)WG+7MQ74+tO#k8g|joPX{`5m{=+o96Y+BDUn9#Ax9{+QG6y4 zUrW{b^n8<=QPHsD^(Ud`lVpv-5q;fZn?T;;bSosoNWR)G#&if^417s*FIJ%K@oggFP-e>jrpiWrNhhPspE zYJg6aBQk0TEQK=eX78Y=*TCiZ+pv*O6hJ38bcFH}{M8N!fJ&gK7u6}{XuGRzWTENZ z?_1ASXFPu-`1|E_fy;#>e!Cb&$1yn}v@~aYnfrEuYSDy_>;+MALe*hYKc;Bor496B z#Ktu^AAkW=1@D7&`aj%6s}`Kl>7>sI`#|fH1I%1@hCj2B{xJ&&%HhY?N1e6k(Xraw z-#^p^_WxYuz@PVK{LwV?kNglh^>mc_)`UDCcP#oz-NRGJ20)5CcI$&e%Necn<6U

Qu2jJeR0Fet9})!&bZ? zdM8pGefmV`N@D!y~xbVSHjtW=`CIT;!wa#Ry9E<>lv+&`=FC#&G*D66j9-7J;CjH zkg}AMzZ#t!z0I8&jeG63>I(7)-?!>3C4=w+qXJazq;T*`78!8g4wjmr3RHe@?+>lg zG+FZEvwopXZwytkhbu~oQ+{#oit|mM9l}~wg82`Egks9H_IFqH6En8 zF4J6(kV*!5!;^KfjJzX@rZp!DnUMw?)G{*iRj!(eUuzhCv#}qpSXDFZ;>vQfvL9T* z!eXI15iVkw9h6tDZE<3qXq=v-t(se zGUtnyZ(%XBh#0VmzrRe7S~%dZ`}qEkrknq)?#Jo$A6O$l6K?;{F8JS7U$_d>N1FD# zl|{VBx443jQwm(Jj09xRFB#e649`=tY!Iu!+PZrtX(de#fEI@!sRsazl@R2X2I#@G z4FN?tB#FQu|9%9r#Ak#W#119`s)Xcc7J94)y8Po?(OV~r0rNxwctDYA9XQzDQB%OW z-~j+#8lJL-v{Ks!zrOs^sqSy1sz{^`KVT~MlpJzW*sH2=|!))Rp9*BS!LBLB~TS5?{9e*y-{g!*(~`)B%R z%k%6NdAASl6TGxXFP(Y*#6pY1nlVmN1`u&F#q!prU-c0&Yu4a|HS$m z_dox*|NWW$e_Vm5h|~ZJG5cQNp^K@&Q&TjUFKDa>w#!==Kgj?2C;#_nec)l3z+)=p znd{F#uD?IC{zn(X7cOmJ(eW??c+z>j^uIr+fj3A2ixY151NT4wxc~iG{6D1zTc%Ox zjE2r=+8NDRpu%ai+!-yEhi@feQRDZ)@nr?H=CgkFv0qNpPOV@5^PBm@Mh^8{T+_$#~|$aSA6zm%b1{!Mk@m4%aE#@+{Vjt=7 z0RUrTKo$Uit-uy0UVsH`F@gU8CJ}(`dm8{OnM8kW+cU}h48sfn%)5Sr4|f4re})Ig z{9X)xzW(WR>wN%V2k)>d9y+8H&-7E9iJ1jR2>agV1;6`{{tJwfqGCGJZ=@?d<6T0E{taK0_THgvnXBp5bX-fC;3I znf2neYgacBcK7hPepdg)-gDMAd)cP|E^zGafFf}8qJ#If<0dAjzmNL!=Vtr2Lh9S( z699C}Z;lngY%?1zbUJ>9uyvzO`pn{QaDUm_cG=O}0sMvvR#}H@*Fii%&H{jmzt6SJ zb^zGsyAC1+wxc)OPCvENH`|wfYL|Y8F+ZyV!n_09I~|-aUIyEr!S=yRzq{}BySDpP z-_7T4mQC!r96c<|z|S3E-3^!kr-4I&8lVhv0Gzdd16JibtPbD_Tmx_aY(_9z1-MnDjU~<#|SC){E?yIj@RKO3TVCD&JJq zH#9aix4dh8-__mI+t>ej0EZtNpP2kQHT`XdxU{^oN?IeYZ-DFhCvw2QKT-5MdU!y3 zm|0m_SlKt}VPf_ND+>=Ro4C>z-s7h17rl1vJ9K+1---BVMRgn!%4Z4um#%ki6OdHF z?6Bm&%^$`8tW1_YQXMI@-*0J@FlB zMs9qBHMj#UIIk_^Ji^HUoFPCH-imnAyF3o{Vg`lli_@wSw#VTy?H(N;_OU&fbe_P~ z*-G%Q?8i2FhV}=mKEUT^N2uf=^?4V94nW1#UZp!p0mB!BgekRhuAfDEo@R-D3ld2v z@2~O5&#H;W+wC^64|gfWY&_;0Q;Q*1+pXTp(E@d7Hh(je$J=gzQ?EWt1 zQI&|v+cC-17`4lV6}tT$9%ZVRC#oR^ruHtGNN2J}VN^ysxkRs4;BW-Wn-n-Lx#TV> zY>0MyTBJxRxR&XoI)N6(x^lCVl}WBd&v_0)8Ur{@yt_xFI1wM*6!2iMRq`BdAc1$( zcO&w^1-DOL*+LrSiG98E)4YXel&)hUg7tfPOcDE#th9U|L@ZVi=}RbXH+WzS=V-5h z*WNf8kvpib+4rD*+g-cd?L#%Mq_D)ZzBV7Kj6Nv*E-+^MI^G1g_fR?>8ZbT4~CnCAv1 zLexsr=^G>*Qkob_)F1042jT-^%BNH7)%By?Rbgs0q)1H#$L`=W?Mbe0Mg5kae#R+1 z7(hrhb=`R~mX}!m(5`&=QoO8tI}@@{1|wqXT{y~Y)N-sfjl%HcL39^er!oHv|jyqH8RT( z7YTP=l|#p1w;XIrt|t0L?d)MoE*n8HuaBFw49~2H(JgA^u>8?2PGGO^%`f*11|`G5v}36WR8=Y+&64jSaae2C$8Kli(iS;4asM z5~3R8gZ6Ap!3Edk-EwuM()JBYEMH7R$RRb7t%5t$c48jgCi;_V$YKOn7~2r}fnE1- zdy7c4-(f4cK#$jX+Ra)P(*U1Uz=KZddjcN9w_51AK=^Cb#%o-7?yP*0CFBfgxy(b=XF)&v@V>_s8r1P zRJSjTg}`dgHRJpo!%s^_dBC{oT_~|&Nvgo>j;t=Tw`<})wR(n)MYD z3h{*XsGjn^+fv>oVsg`-Vn}zgAz|FQm=G6DHO=^@zL0nKTb29j%HcJu=tDq=^o6C% z(a1<=mR^~{H0l*ctazq_G5)^?I zbl3=suO#tl(~9zW`(YPJxKq|~5dppG`#U9EAUv6fSx!HI7bcHX4k8qw22$$~ETu-y zR-}+3;^9>J-j{di(%0N}&bTN%3)GnBy|Eog#$D02tsm66&KA7U;OTi)$hNsC>{5#} z%fXvz8a=62epIe;VG{ogUiqewyG)l~4Wg+}b40M(N${f+Ut*MZVgawO_ECjgkJv%C zjBb50Bx;WY*_N=}2rHSIM~!{-)+?>~;vXVJSa81py2bwUm) zBrKaNoxcm_G0%=5jtt}%QZEwv4dRR|D`U)uXH3q2X}eZbu6TvVAo|#PdEgEhJVy~G zn=|$4+M}ZldMV`cwF{-)l&CW}m&%M=hB?v|NvRA#nGl7aYADhc?OwYy>V?dYRdR}loCrIBakS&G>#dP^y6aEt?^jMT3s zdtRXf6}ExZVsMIlfh4^Xz2->p2aXQfxs#F!Ze#w+Lfufbm|?p=t%NvblD86?89Gp_yt*TX}?Y=tRX%vnSTY_fC$kRBfF0k@&wF77gzJW8KM`X!oJ+X1MJ@=ta{=?z> z$Ic}$uwJ+kJvXo|_jZ$0fN~S8B)c&V=ZW9mKX*gyP7|IYZ{fSvIZwS?(m7Ewus#24 zuU)YyRy-pN{`8cx`|cDP;>D|1Q_d`!Pt0|+(8bqsMXL?vY?Mx!PL@Q*?E}{BY2v>ckhOQn(+nTU~aJgO8Xw_(tbRFn&;@&2x8x zctrAXna}$JroCGB^opN6xcl~_>7WC>lMc)L5m|a@`6v8^z6J3{C!d#2F8dXGp7gy) zpnp31Xl)5pj$r#IZor$XBf=X}hxT-qx96$YoIf|4+JEkhc4Q-+{~M`~FfiESa`l-+ z#EE^ex6LnXYwAa;W(S0gpQ-Ett(?Z_Y`~tYXb-Ro3;zIr6w!zJ6skiz4mUgEZ8L#*m8t^&pDlYB4cbV z0_*q@Lg&aGp5r(;p+(EEwGWk;WA#ela_D0xF zANd`_aua%`HL;7IKIS!bmg_6;%T;&xh4?1r-{=QD2A=s&TU9Q;T2n2uVM8x|; z2$myEsQMQK$US+@11ftoh@_H&fZ+amTw;z>c9iYNcFp*iD}n3hZ0}uAidOe<(QMq7 zED0SLDHfc!9mNi^;O*KQJvnSJPrWl;`*B+evs+cvq}tDRqc3-I%n#s`wP!Qqi!Alb z>fQkmGgDnY;18AQcHyj``YBs|2cR--!Ba}bX{ldBcwNJmWow|N6A2v)o91_60Jmd6 z*Q%^1FY2Ay2?kL36+9Sk#zK5w9bT5&|>9;M>WT6r2rMP+6Zd8YCHv?c1 zWdPAuj=F#s78iVk0i;`G)3>NG0HF*DyalqdtrS$mz|Q0R%>fTcku-FTC~-#{UNEZ0 zE8QhnuIIFyTZY|?C1K>ll`+M{g)u#7CxA#T3zYxc@diwf?e_eUO)~UuVhfEqA4X8Z zlAHW2-jN4e=qzNHXAGboP879$O`xZKZDPl*$~~UnyL{zB{EKyKEAOIhxlbP4l3|AZ zNR#Og%$s|ezC-Otiuh2!=Sa1z3`@)p&xicTkAnA+z&PrMIne(ReYxW2w~mbbgTY(9O{h@|1CHk4ow+XT!DP{SUoP8M0YTUVh}Y zT8MRKVL8^P;CuN^?8X;cxQp6@eR7NE=d6S@tfCd?0}P-~e{vmk zp!FdKz1FrPi-^i}4llwoxJj1Z#gxW_f1uAequL8k3;jV zd_f{#jNM(&uj{5xE~hJIwD_3Fp#&ZJR!S~amIx2g)Xxo=JrS?Tfq&{<-}ibtjW=9R z?Y=9i$d)s=RiZlnYZU^DEC{ydSU;;=i=IJ;AznkNa>)$f$~qQ*km5vDo(eB4-5qqN zjowdjyepvXxj~{FZkG;jA92#SuJmBzfG!Yx)D258v{N5%u!q!k)>yn~9|Qe4`OrDou!ErT8FAd-ju?r@06SePAraQfK#goo0K-JcH6&BK(bmHdEyB1Mpf zQlmfkXb+=YM73rj&w}Qojx9KU2i!hu8ZrQJV@^62lu|J z7zR}d;v73u2z#qae&m=1ANR_KtCVj@rbwJO5^)RtzyL1O2X@+&E#iz?zP56wJUfJ~ zG5;7*()M;R+M!&Ag~$NvK#RG)@GgQMBjl!r;$K`vh2CrlN4h&^&j#Iw3lmosZA*$wSXLt@N> zjTP#!dyYL&1a05O@W=5$_|DYYZ(<*IC3GCrP2yH?+c+f?4P4lv9?ZvoR%hoS^*|H$ zap^E^Ir@7P7gCLg9!1PwBAq4YHrgNVTkxnksvg!$)pdFK#iMI`X!*(hfD(#o4SWkS z`)*9<<4+?B#ESU?1#x5b6y zkW^dxAbn$N1HWS|_NI-Hjc)RN!DA=Nj{>VLlW^R#id%<{36uDks7pv5f@d5?9E=t6 zY0H2U)C$92K<+1UN3OnzYV?0ylky_!aGcn~_(KNy9ujtXcHcNoR?38po;p7Cby$U# z3C2tXV42I*$+Ea7$x@MB1_aBIXPedILTaQbCy~Cy!pItnBi)*g+;{W#miJF|A5_WL zCOglZS3Y>(x-sE&*Lj4Hy_=i&fxFp9*AqXHTE^jXO2ljgadsf!8C^7>ouXEM^LC$y zrg^!gRbq;KH!al?6T%sl-tH}96dvdQaM(tpCCu|1*DbxSBk4f8Y&6p`PF+3|rvKd) zaesurv<=Hcy-drf5p~@DWy+>Y6XLk<-0AW)?bGqY>@stC>}_@HaCZKUWrQs9EGkc%*nv|ojW=V_Vh3ju`US%kUn^3=7kXE}gcrD!Iq-FAe!19V-DKk5ZF(n6|ul|FT~7SwuY z(e$JGKVI5d@G-srL$`;he@6L-FaOBPKkMjU*cpVY?Bg$HyzMwA2h7(-RU3S0WJ>s8 zs0kd^VskOo1^9kfQH=h--~2FMv=?;d0R8d)Tc~2HZO-h5%{OiL3ElvdO>L*+p{<{R zrE?!S4bXE+a%or^kbaL~#Zvm-xf6iLkHPkfsD zg_c~HpI%v+Ae1H=ekb^I%RZ!WS|s)z|uL>y?7^T~rmA_(S?C|I#oxVC&iAgPFe373uz zhJ3pzP?V__UQhrlUbRK=*p;I_vF*|AOcifVNY`T=MB@$X?{g2@$Sre|cGIe;Runy? zCDE!50;$WepAp%S?39O_4HmZT?OJeO$V?EdaKuC&c4B+f*&8-&9-Q`$WFuahN5t9}zkN zBV;iE57fp%``zF%aB7wTBs~o7L@fDY0qmSZ4{D_@&+5zSd{1NBcAXtLdfF=NngneamTIr&1(bbuwhRhnqtD z#VWN0XeGg10XdpHzPn4lkoVRSTM3i_q&Er_?uK7FmvaD4#Ali8%`@6 zbF;v7&Y?=>>D@1FUcdk7E(-$9S^BRtuccp<|pb#*&za+kTDi}rP!+uz=pz~qp zL0omC8qj1zJJixr92vA9Dqu7L=nfQqzb^x5drwJ=gq7uz!s(pZpm`YHdtjXbjDk?g z_`d*#;f?5D7{JQ8nxi0QkNOz^RBqv>485-q2O%q<8Nfb>c{Vs_w~eUnVSfQE!vI2| z6mJI5d5JJZ=e0smYYN{?XHaag&ZyBU6!l)vSu~o?23^(``ZHw4pUA;XJ^xF}$qs)4 z{R?9Lq5PjY|35D6dGviq9fD`QC#x%hhc1p#NPX_IbA2u8Ox`S)4p6jJ__fC9HLcty zL~IM}E^LP;&qX4n*Arc=+zf z5y<8K21rRE^{Hn|J4I+^gr?}c+G(`wpz*euJcorQRcF1!NCLu9A!c3peL|KxGG=DJ zItYT&0k?(cH9DpL6@er$CGKwmJIsk04g%TP?JqtV#Eu<~h+Jg~J5u-w8ig*FZ*TA< zOO-9oDN(e@!SLOK=p9CQqpfANCq&iGc}N8G%>>>JwNg~(fcfscWbO|1e*etpakAm2FE|^^*p#{hxAzxF*)I&Nsk3k5B#!5>FyG zxQ)S0*b&KFO^9m*({c;%A>rdgh%s}^ovxU@{m=}0Po%s!bx6xyq>Izpo!+Xp_8FCRzcZ;bnB^s z5c}(GUW9JRaCmk8^E76Anc7aiHyyTCskU$4`E&B-B5Kh|QeCZ=?=o*oq+#A=ekuDZ zCOsKGuSHR&S`!PSw4tsHfZJ#5Lf&c|d%%|Nlc7=-x9o-kJgs{d7xGOsUPjzie#?_N zz;g%?oeaXL5hX~L=NZ4_s!x%aAAo|4yI4e&bd$d&@dMj%5DT)oV6KNecrWNxw>(#d^ ziI31wBP z`_mzP=)47S$>5&j=4``-cSjf8$vgnL%?df8WS_=DK2k`4(vGsE@({bgte-*aH48pd z`-@$_SGND7Z4{Vc@;6KO-}O*rN-`Ol7>r~AHxNzOI|xq9k!d+RJhDpcMfD{xNw_9L zDo&#JJU_Jw394p0dT943NGPG7&gqUKHwD#lr51uQ#n&SrET8&yeDLydXw}PmUM8uSi=TPe; z%mfmQD1xX)z^0SP;3L_}vD7PZWWVe=(3^V_%(5fPmx10E)wM@_zdkj*=QRUZo)3CQ zR-=pMPg zqvfD37u`Q>Zb~bAjOsYa00>f$vtSsby=+ZlbmULysh}FlV3*(<`{^Q}%h$aSOi>2Y znKCGDieQK|0@NT+YiETs0JPh}zszKPQXh!y@m-*+;Bk-6lHg6u$z7Jy46#`HV$!eG;n$)Fa_=7*-bVn%ss2axSqf5C=5 z--WyZvT_;;>iZ$paJuSuAqSQ7=MJD%#*af0Us1w8Y?yux`YWMZ+Z4JVfH!6C2ZXG? zXghVqDS!J*{KK(-1ph1i@ypr|Z=g^8d7)|jnb_|OO$nTe-x#^ZoxG9(lJFOTk6+vr z{2#$rvg=VHXin`dWF*-ZuNUF+?3}WvqILcKmIu=>`SnNHZhC$n&r)@Hx1IBnW$pom zkL6L>VOi_}$Dt>Ub#*{iEo(n&y!uP?A(JZqPbiqM_L%>~jbpn^P1ez;G~2U)|3WZu zA2rt1{ko%PbnF&HckkYntY4#ln~^|nF!||kHf!em$<;)(AR2Y(_#Uq+y=}_hJ{oa# zd^uS%SX|?AC@*_&+*PZI3M&6p-7p_Hbky@Uot4=4)HBz)s-+pebzsEWPH%t6*sxQw z{n4F-_wTPAAB|kk7<&TYLX9Rv$}bB-itVFFQ0^HBC)IHbD-a-Y>S0zA)y#!!BWS{M z{g1x51~^)%5bL=`3&;BQHk3MF#ZSKMs+kP;q- z!td!Wl{c|z@pa@9ymTXFtnd2AEskbVCn~Rs-FZwlqnrxv*VgRNG$S~X!icqtA((xx z^esKB5_lS-p5;uS#_V3S^8;OMthGCI?m3skg(Xe$;7g>(PLs*L~3xo@* z#rovi%+BM&Qa$?n1UdtJjQIrCBvoG3J&cgzl1%2XdN-CA42C1h_2(M`32h_~+6#;l z8LQuq)FLJ|Hq_fBh3BtaE)7JKwjkXLC918jU*>kkxC%7}in_j(tGxw#cJnO}8cCHO ztp$zlX7xt}4;(l1L!eRy`<9)2V#C`|d8>&|>QwmTkiTN~J9WN^pLk$Gjpr$Ig9 zT?~`}X+rOG#c`?ekWUy(pDd4+3ru#u(yhuFp*htf$uHQG>-qiB=qpL*R zr8wwy%v|Ki9+WF~FyjoC_7WqFlqbfSEFT8>y+R^Cnx_7kl~J$wrMk$tCqwPLcT;O8PlC9iNw99$DW%!oDZD$>*o2>{B+U zFGqB9=hxgsRtcGrKvs5p^|__$^)sk_Wj$6*&yA`Zt6|@U;WQ38EV!K3JxP6~(c}y`R0dh0k1ux?-Y->F4+aoN<6w?9*wxCLIH;Q7^_Gv`<%AhzTYR; zII;`o6eIUm1U+u%K*s2{)*rn2fXMYKiY zt>(K&CMSxyjKqZs2R8mTtFW=3-$q6U)qbW0QXh06v5#I86NNO{Hwa$#6 zzsl=8#*b;`2D>`{{7tTk37hB79kjoH{YVwGQ8S}k#Y3wK z6oX#j%H3qgj4LRp&xo(CE5;oiY$?A>W`}L2ng09fm@IWLXY@eaXd#$!mk-aAWK8czu!1Fy*?l7%V!^bfF-kXbn4LH&K)d2;Oj zI~FyyY2#+KwV{Vty}t~%YA)%iLE@hjYRS!*+U>>3=f8HTF}#Gl&IZxk@j7L2HFaz*Z!+dnu~wh=8j2v1Wi}4os7qfPuF2>+rckf~)6N zBf?a}+0Wd-u&W?_kA6>Kfkby!uRFc%y0~G1n1FQnU>%?Xg$;-u zNH#)}E@9*W$nr4tQk$u;eC+|Bk&D<{r+Mw>l@iW#Mg&_YcYmD>I3ILe7eI=Q!pKsd zb7qA0=9`c>q%fYt-IFSxW0dRciYv%aElBcKe^(q7i!(~BsEBARuo22Q_jrE)p@@pt zRkRS4BWEKqsAgEu%YD4oqowiYP_e68j=R^ZZ)^LOM6*JjcpAhjnGH|Nrg;HIHGtfl z2*}aJO{HH8=CF7V#qUIJ8(U=0?<)xmbkDJGgnEp17kXYLtqTg;Aid=uedwICD^%I> zh5LA%ZXdW@u>)(3H}xCy*{ul-;He7{>CjX}do_tFWgp;edG($PHciY!RW5BfEqWvL zOpghtD8yw41nQS|s02_`jf5O3LNpTxcc0?dJ7f43xbAJ^^OJSW6}+=w>*W(YFFRK3 z%t!B(WLh_v{-KJW8XBuXb(f+y_R%4`*LGUJTj# zZZE0u7N{>XfRdNA7+aDEuWLiyyf+D5%K4Dqxt&y4#5jaYll%S*ah0`%9coA z$z5Z#Kh&o%#%e_D>O`fTC1}7D6J`fPLOSISN>^4!KWF8F__l@k&*S*mge1vv;jl|_ z;c46N;IY(wA`2DSc1Wc}3PpY9sCj%Ht0D@=yQ}@g&H(!tV zQ5gv0cM-FH%ZT?1R_l{j(cpBeZhYGDg_U`jLqSwSlzhqZ!jakN;76YZs5%{>4!mar zP2aW%BY*Ux?*;c`PX=J8PY>yt)qcp-u20-`47B!wKqHFHA5FcI33~4%(oyZ0>`k+w zovvh8>boDfh9wlKaIdd{=LEs(254<>h68>C>*Zqtt)2SD4%pO)87O3SQ=6%_bOEW` zpgs6A0!{wrChVLHfb)lEUWy~1OJS!{W>93gn$3jVAuwG5N#O?@QvZWFMT!`Ex~q}bXiKU@L^{yQ)v3w*(&KK+zFfeouK7qo zSf$aZ;Fw3ERm_GReKDvQH@?)`(pO#egP!qqal|7hZsQ8}LtoaL_kG%6fKnVqxs3go zYx$Y_{FX}iDH~TB&iB{YGU{J>jo{yY`-Ax?syF59Ds0u*PI7Z6x{2O+k-o#K0kw1l zva$Mr0qDfmo~o(%`8w&R>s#C8rnqra<5IN*c9I_v+63nqJnoi9No^Y# zFT2%NdS@~(AqUB`bm!fSwkJ7CdY+vki)Kd}^kbx4_h@#nqIjp%_Vkwy*}}W$M>+Gq zv|M`dIVcjx@yyvN@$k|_!l=r{+v}K{01fgSd#DsjmJy7F&RrqLvPhR<`?aARB zd?2c>D)yCc+vT2&p3F22y?~WsjN71&w@jq9X48Flr*p}IzCbX#6Tu#^i&Rc7Bl2?J z(-tR;G_a@Y5cHlEIG1-bfUCAGUawqjwD4t|eBjSqc-dCvYJ5ES-a*zxe{TaiNgH&n z)#=j@o~QDWU z!C;~?zWflM_W5AzFYQ9~VS5F#As!;8){F>`c@a=iXA_*92Odl_5vVO0*{&6OpN`5e zMDL^%_v>3r1X!^u2<2``=kDB)n?!_QOQmBpy@_UxFjkC0z;UG5nDMSNMSba!V#j?o z0&6u)62|y^A6*K+8sF;Cs^VyH*2+!&Nq?{TAK$0=<(ldC822ieOSamBnwCFP^39mY zzLARBn1QVbWLvMKZ+|>#eX2ZClVyYNO&GjwiMNGPl%gwJDQiV5IPd%_V-tDFNNxjJ65s&w0 z*;{NL;6AMIi~;OdHUQP4D;W$R!(FbbK+uE%BqV2|J}ZMhozC({LqDV;u*>D3-Z{|+ zpv$g(Wt5O&U+6j)=$qb8cem-oUNhN}5QOtp)t*=Xl}^~-r!W6|p5{L;+my$&K@;`^ zckm}6lS6*d}0R&^5<(>O_Q1>fg2DS=G)i$ssO zJ#|!kvn+6AU&!Hji^mt-3o$R#xmy-Pz~qz`EFV(47PQHs0;tOY0^sZa0<~kd?b}B)xx)#ocX$~oJd3G0Q66+Qct+6+AQbA9}3V{|a^v;ID67EBNH3*A9lPWs;A*86!4e@pQnNN71nc(mG=;fdn9-tP6#E%h$U>B$g?@9QXVlfw7%7c;4gJH11w{PfZ;*N;<@Br5^loH1 z9eTGWP4;{B3P5cVx}y~QK_UbWZbEy8Ev*QuZa5q)MsHv8Dw#{N64drj#XcPl9!lME zc*^S&du8X3`AEOlu0V|{+%~0tK!x+3{0qvlw(}>t3x;BvdlM9HdZQbXT>G^ZwGUBSCwd^J>m0Na4A7#kIn$VfD3JXKfDAW-Iln+ud#%SooS^ zW8S^ju7P|r^37h*l3}T$k96o$F@vB3sEGlHz{v(a-0SoT27ryCt4_9Ot+nv8lZ=6z zEmTQ$b}9p~{TUF37YA*PjS)H>O6~{Mv&)^d2gmjlF#wJopveL21HDH?RCC-9!%cvW zC>7M@4)#1$y$)#QtYT?Y1TlloE5<@c*XHmDRp}YWei4_dY1EGp@VI@4803#)bI;ed-{=} zw*nSUC&z*3SYGKj%UxN33#$J37;2sRGZzW>1JHC7_fI@Fmq`bm4nN$|r%LXT2KoH! zpxb1a8NeTIc7l5edjvRjpu_9?)Ty3`1i+Zpk2h6*c(Td!Ti}uU4=$T>{(CO`|G<|H z&EsQnzlfkomY6Q!`r^%)LtPDQtsX^G&E1k+aO!ZIPa{Ovl$!WvW!BcRRu}LQ*y%pi bU+dFRAQ)L+LU~?+pO#@B`rW|}VGR5)d%J^m literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_4.jpg b/docs/images/control/control_api_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..35920b82f2e89e3d2aea976038a9c17e0205d6d6 GIT binary patch literal 34105 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 literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_5.jpg b/docs/images/control/control_api_5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5dcb3590612ff7d538735e08c6c30c2d8e2f378 GIT binary patch literal 10832 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 H=NS4Q)6^Hg literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_6.jpg b/docs/images/control/control_api_6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3693b19b0c1762e89987314cf7b6e695a8b9880d GIT binary patch literal 18704 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 z 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 V`HVf`Oe>ex#ZGZvFlT2B|1VCMULXJf literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_7.jpg b/docs/images/control/control_api_7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..132823db388df97fbb5a3e5b40f89a4d198e1922 GIT binary patch literal 34418 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 BR-OO= literal 0 HcmV?d00001 diff --git a/docs/images/control/control_api_9.jpg b/docs/images/control/control_api_9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4893ca4253bd6d779532bdc881c633b074ce8488 GIT binary patch literal 24809 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 Date: Wed, 6 Dec 2023 16:55:52 +0100 Subject: [PATCH 34/54] FOGL-8250: call service restart if bearer token has expired (#1230) FOGL-8250: call service restart if bearer token has expired Allow shutdown before service restart Fix for North service restart --- C/services/common/service_security.cpp | 2 +- C/services/north/north.cpp | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/C/services/common/service_security.cpp b/C/services/common/service_security.cpp index 3504ce7db8..e1c756b92e 100644 --- a/C/services/common/service_security.cpp +++ b/C/services/common/service_security.cpp @@ -608,7 +608,7 @@ void ServiceAuthHandler::refreshBearerToken() // Shutdown service if (m_refreshRunning) { - Logger::getLogger()->warn("Service is being shut down " \ + Logger::getLogger()->warn("Service is being restarted " \ "due to bearer token refresh error"); this->restart(); break; diff --git a/C/services/north/north.cpp b/C/services/north/north.cpp index e9c81fb15e..d2ed9a792d 100755 --- a/C/services/north/north.cpp +++ b/C/services/north/north.cpp @@ -533,9 +533,17 @@ void NorthService::start(string& coreAddress, unsigned short corePort) if (!m_dryRun) { - // Clean shutdown, unregister the storage service - logger->info("Unregistering service"); - m_mgtClient->unregisterService(); + if (m_requestRestart) + { + // Request core to restart this service + m_mgtClient->restartService(); + } + else + { + // Clean shutdown, unregister the storage service + logger->info("Unregistering service"); + m_mgtClient->unregisterService(); + } } } management.stop(); @@ -716,12 +724,13 @@ void NorthService::shutdown() */ void NorthService::restart() { - /* Stop recieving new requests and allow existing - * requests to drain. - */ + logger->info("North service restart in progress."); + + // Set restart action m_requestRestart = true; + + // Set shutdown action m_shutdown = true; - logger->info("North service shutdown in progress."); // Signal main thread to shutdown m_cv.notify_all(); From 6225791ac0b0ffc91b80dbb19a12aa1f94daa7e9 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Thu, 7 Dec 2023 17:27:10 +0530 Subject: [PATCH 35/54] BucketStorage type handling added in for its microservice restart from core management port Signed-off-by: ashish-jabble --- python/fledge/services/core/server.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/python/fledge/services/core/server.py b/python/fledge/services/core/server.py index 9faa4d19b3..ee4bd61c40 100755 --- a/python/fledge/services/core/server.py +++ b/python/fledge/services/core/server.py @@ -1303,32 +1303,35 @@ async def restart_service(cls, request): :Example: curl -X PUT http://localhost:/fledge/service/dc9bfc01-066a-4cc0-b068-9c35486db87f/restart """ - try: service_id = request.match_info.get('service_id', None) - try: services = ServiceRegistry.get(idx=service_id) except service_registry_exceptions.DoesNotExist: raise ValueError('Service with {} does not exist'.format(service_id)) ServiceRegistry.restart(service_id) - if cls._storage_client_async is not None and services[0]._name not in ("Fledge Storage", "Fledge Core"): try: cls._audit = AuditLogger(cls._storage_client_async) await cls._audit.information('SRVRS', {'name': services[0]._name}) except Exception as ex: _logger.exception(ex) - - _resp = {'id': str(service_id), 'message': 'Service restart requested'} - - return web.json_response(_resp) - except ValueError as ex: - raise web.HTTPNotFound(reason=str(ex)) + """ Special Case: + For BucketStorage type we have used proxy map for interfacing REST API endpoints + to Microservice service API endpoints. Therefore we need to clear the proxy map on restart. + """ + if services[0]._type == "BucketStorage": + cls._API_PROXIES = {} + except ValueError as err: + msg = str(err) + raise web.HTTPNotFound(reason=msg, body=json.dumps({"message": msg})) except Exception as ex: msg = str(ex) raise web.HTTPInternalServerError(reason=msg, body=json.dumps({"message": msg})) + else: + _resp = {'id': str(service_id), 'message': 'Service restart requested'} + return web.json_response(_resp) @classmethod async def get_service(cls, request): From 9045371dc62a7dcd925f9737fdaa8b0c5998c076 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Mon, 11 Dec 2023 12:26:00 +0530 Subject: [PATCH 36/54] removed gcp references Signed-off-by: ashish-jabble --- docs/plugin_developers_guide/09_packaging.rst | 1 - docs/plugin_developers_guide/10_testing.rst | 19 +- tests/system/python/conftest.py | 46 ---- .../python/packages/test_gcp_gateway.py | 234 ------------------ .../services/core/api/test_package_log.py | 4 +- 5 files changed, 12 insertions(+), 292 deletions(-) delete mode 100644 tests/system/python/packages/test_gcp_gateway.py diff --git a/docs/plugin_developers_guide/09_packaging.rst b/docs/plugin_developers_guide/09_packaging.rst index 99fc7a4ee6..8f99b011b0 100644 --- a/docs/plugin_developers_guide/09_packaging.rst +++ b/docs/plugin_developers_guide/09_packaging.rst @@ -121,7 +121,6 @@ Common Additional Libraries Package Below are the packages which created a part of the process of building Fledge that are commonly used in plugins. - **fledge-mqtt** which is a packaged version of the libpaho-mqtt library. -- **fledge-gcp** which is a packaged version of the libjwt and libjansson libraries. - **fledge-iec** which is a packaged version of the IEC 60870 and IEC 61850 libraries. - **fledge-s2opcua** which is a packaged version of libexpat and libs2opc libraries. diff --git a/docs/plugin_developers_guide/10_testing.rst b/docs/plugin_developers_guide/10_testing.rst index 4b2d8a4030..8c80c0b88c 100644 --- a/docs/plugin_developers_guide/10_testing.rst +++ b/docs/plugin_developers_guide/10_testing.rst @@ -72,17 +72,17 @@ and their versions. "name": "http_north", "type": "north", "description": "HTTP North Plugin", - "version": "1.8.1", + "version": "2.2.0", "installedDirectory": "north/http_north", "packageName": "fledge-north-http-north" }, { - "name": "GCP", + "name": "Kafka", "type": "north", - "description": "Google Cloud Platform IoT-Core", - "version": "1.8.1", - "installedDirectory": "north/GCP", - "packageName": "fledge-north-gcp" + "description": "Simple plugin to send data to Kafka topic", + "version": "2.2.0", + "installedDirectory": "north/Kafka", + "packageName": "fledge-north-kafka" }, ... } @@ -118,8 +118,9 @@ and the function to call, usually *plugin_info*. .. code-block:: console - $ get_plugin_info plugins/north/GCP/libGCP.so plugin_info - {"name": "GCP", "version": "1.8.1", "type": "north", "interface": "1.0.0", "flag": 0, "config": { "plugin" : { "description" : "Google Cloud Platform IoT-Core", "type" : "string", "default" : "GCP", "readonly" : "true" }, "project_id" : { "description" : "The GCP IoT Core Project ID", "type" : "string", "default" : "", "order" : "1", "displayName" : "Project ID" }, "region" : { "description" : "The GCP Region", "type" : "enumeration", "options" : [ "us-central1", "europe-west1", "asia-east1" ], "default" : "us-central1", "order" : "2", "displayName" : "The GCP Region" }, "registry_id" : { "description" : "The Registry ID of the GCP Project", "type" : "string", "default" : "", "order" : "3", "displayName" : "Registry ID" }, "device_id" : { "description" : "Device ID within GCP IoT Core", "type" : "string", "default" : "", "order" : "4", "displayName" : "Device ID" }, "key" : { "description" : "Name of the key file to use", "type" : "string", "default" : "", "order" : "5", "displayName" : "Key Name" }, "algorithm" : { "description" : "JWT algorithm", "type" : "enumeration", "options" : [ "ES256", "RS256" ], "default" : "RS256", "order" : "6", "displayName" : "JWT Algorithm" }, "source": { "description" : "The source of data to send", "type" : "enumeration", "default" : "readings", "order" : "8", "displayName" : "Data Source", "options" : ["readings", "statistics"] } }} + $ ./get_plugin_info /usr/local/fledge/plugins/north/Kafka/libKafka.so plugin_info + {"name": "Kafka", "type": "north", "flag": 0, "version": "2.2.0", "interface": "1.0.0", "config": {"SSL_CERT": {"displayName": "Certificate Name", "description": "Name of client certificate for identity authentications", "default": "", "validity": "KafkaSecurityProtocol == \"SSL\" || KafkaSecurityProtocol == \"SASL_SSL\"", "group": "Encryption", "type": "string", "order": "10"}, "topic": {"mandatory": "true", "description": "The topic to send reading data on", "default": "Fledge", "displayName": "Kafka Topic", "type": "string", "order": "2"}, "brokers": {"displayName": "Bootstrap Brokers", "description": "The bootstrap broker list to retrieve full Kafka brokers", "default": "localhost:9092,kafka.local:9092", "mandatory": "true", "type": "string", "order": "1"}, "KafkaUserID": {"group": "Authentication", "description": "User ID to be used with SASL_PLAINTEXT security protocol", "default": "user", "validity": "KafkaSecurityProtocol == \"SASL_PLAINTEXT\" || KafkaSecurityProtocol == \"SASL_SSL\"", "displayName": "User ID", "type": "string", "order": "7"}, "KafkaSASLMechanism": {"group": "Authentication", "description": "Authentication mechanism to be used to connect to kafka broker", "default": "PLAIN", "displayName": "SASL Mechanism", "type": "enumeration", "order": "6", "validity": "KafkaSecurityProtocol == \"SASL_PLAINTEXT\" || KafkaSecurityProtocol == \"SASL_SSL\"", "options": ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"]}, "SSL_Password": {"displayName": "Certificate Password", "description": "Optional: Password to be used when loading the certificate chain", "default": "", "validity": "KafkaSecurityProtocol == \"SSL\" || KafkaSecurityProtocol == \"SASL_SSL\"", "group": "Encryption", "type": "password", "order": "12"}, "compression": {"displayName": "Compression Codec", "description": "The compression codec to be used to send data to the Kafka broker", "default": "none", "order": "4", "type": "enumeration", "options": ["none", "gzip", "snappy", "lz4"]}, "plugin": {"default": "Kafka", "readonly": "true", "type": "string", "description": "Simple plugin to send data to a Kafka topic"}, "KafkaSecurityProtocol": {"group": "Authentication", "description": "Security protocol to be used to connect to kafka broker", "default": "PLAINTEXT", "options": ["PLAINTEXT", "SASL_PLAINTEXT", "SSL", "SASL_SSL"], "displayName": "Security Protocol", "type": "enumeration", "order": "5"}, "source": {"displayName": "Data Source", "description": "The source of data to send", "default": "readings", "order": "13", "type": "enumeration", "options": ["readings", "statistics"]}, "json": {"displayName": "Send JSON", "description": "Send as JSON objects or as strings", "default": "Strings", "order": "3", "type": "enumeration", "options": ["Objects", "Strings"]}, "SSL_CA_File": {"displayName": "Root CA Name", "description": "Name of the root certificate authority that will be used to verify the certificate", "default": "", "validity": "KafkaSecurityProtocol == \"SSL\" || KafkaSecurityProtocol == \"SASL_SSL\"", "group": "Encryption", "type": "string", "order": "9"}, "SSL_Keyfile": {"displayName": "Private Key Name", "description": "Name of client private key required for communication", "default": "", "validity": "KafkaSecurityProtocol == \"SSL\" || KafkaSecurityProtocol == \"SASL_SSL\"", "group": "Encryption", "type": "string", "order": "11"}, "KafkaPassword": {"group": "Authentication", "description": "Password to be used with SASL_PLAINTEXT security protocol", "default": "pass", "validity": "KafkaSecurityProtocol == \"SASL_PLAINTEXT\" || KafkaSecurityProtocol == \"SASL_SSL\"", "displayName": "Password", "type": "password", "order": "8"}}} + If there is an undefined symbol you will get an error from this utility. You can also check the validity of your JSON configuration by @@ -127,7 +128,7 @@ piping the output to a program such as jq. .. code-block:: console - $ get_plugin_info plugins/south/Random/libRandom.so plugin_info | jq + $ ./get_plugin_info plugins/south/Random/libRandom.so plugin_info | jq { "name": "Random", "version": "1.9.2", diff --git a/tests/system/python/conftest.py b/tests/system/python/conftest.py index 62c4c37edb..72332ceb81 100644 --- a/tests/system/python/conftest.py +++ b/tests/system/python/conftest.py @@ -904,17 +904,6 @@ def pytest_addoption(parser): parser.addoption("--exclude-packages-list", action="store", default="None", help="Packages to be excluded from test e.g. --exclude-packages-list=fledge-south-sinusoid,fledge-filter-log") - # GCP config - parser.addoption("--gcp-project-id", action="store", default="nomadic-groove-264509", help="GCP Project ID") - parser.addoption("--gcp-registry-id", action="store", default="fl-nerd--registry", help="GCP Registry ID") - parser.addoption("--gcp-device-gateway-id", action="store", default="fl-nerd-gateway", help="GCP Device ID") - parser.addoption("--gcp-subscription-name", action="store", default="my-subscription", help="GCP Subscription name") - parser.addoption("--google-app-credentials", action="store", help="GCP JSON credentials file path") - parser.addoption("--gcp-cert-path", action="store", default="./data/gcp/rsa_private.pem", - help="GCP certificate path") - parser.addoption("--gcp-logger-name", action="store", default="cloudfunctions.googleapis.com%2Fcloud-functions", - help="GCP Logger name") - # Config required for testing fledge under impaired network. parser.addoption("--south-service-wait-time", action="store", type=int, default=20, @@ -1182,41 +1171,6 @@ def package_build_source_list(request): return request.config.getoption("--package-build-source-list") -@pytest.fixture -def gcp_project_id(request): - return request.config.getoption("--gcp-project-id") - - -@pytest.fixture -def gcp_registry_id(request): - return request.config.getoption("--gcp-registry-id") - - -@pytest.fixture -def gcp_device_gateway_id(request): - return request.config.getoption("--gcp-device-gateway-id") - - -@pytest.fixture -def gcp_subscription_name(request): - return request.config.getoption("--gcp-subscription-name") - - -@pytest.fixture -def google_app_credentials(request): - return request.config.getoption("--google-app-credentials") - - -@pytest.fixture -def gcp_cert_path(request): - return request.config.getoption("--gcp-cert-path") - - -@pytest.fixture -def gcp_logger_name(request): - return request.config.getoption("--gcp-logger-name") - - @pytest.fixture def exclude_packages_list(request): return request.config.getoption("--exclude-packages-list") diff --git a/tests/system/python/packages/test_gcp_gateway.py b/tests/system/python/packages/test_gcp_gateway.py deleted file mode 100644 index 229f1e517e..0000000000 --- a/tests/system/python/packages/test_gcp_gateway.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- - -# FLEDGE_BEGIN -# See: http://fledge-iot.readthedocs.io/ -# FLEDGE_END - -""" Test GCP Gateway plugin - -""" - -import os -import subprocess -import http.client -import json -import time -from pathlib import Path -from datetime import timezone, datetime -import utils -import pytest -from pytest import PKG_MGR - - -__author__ = "Yash Tatkondawar" -__copyright__ = "Copyright (c) 2020 Dianomic Systems Inc." -__license__ = "Apache 2.0" -__version__ = "${VERSION}" - -task_name = "gcp-gateway" -north_plugin = "GCP" -# This gives the path of directory where fledge is cloned. test_file < packages < python < system < tests < ROOT -PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent -SCRIPTS_DIR_ROOT = "{}/tests/system/python/packages/data/".format(PROJECT_ROOT) -FLEDGE_ROOT = os.environ.get('FLEDGE_ROOT') -CERTS_DIR = "{}/gcp".format(SCRIPTS_DIR_ROOT) -FLEDGE_CERTS_PEM_DIR = "{}/data/etc/certs/pem/".format(FLEDGE_ROOT) - - -@pytest.fixture -def check_fledge_root(): - assert FLEDGE_ROOT, "Please set FLEDGE_ROOT!" - - -@pytest.fixture -def reset_fledge(wait_time): - try: - subprocess.run(["cd {}/tests/system/python/scripts/package && ./reset" - .format(PROJECT_ROOT)], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "reset package script failed!" - - # Wait for fledge server to start - time.sleep(wait_time) - - -@pytest.fixture -def remove_and_add_pkgs(package_build_version): - try: - subprocess.run(["cd {}/tests/system/python/scripts/package && ./remove" - .format(PROJECT_ROOT)], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "remove package script failed!" - - try: - subprocess.run(["cd {}/tests/system/python/scripts/package/ && ./setup {}" - .format(PROJECT_ROOT, package_build_version)], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "setup package script failed" - - try: - subprocess.run(["sudo {} install -y fledge-north-gcp fledge-south-sinusoid".format(PKG_MGR)], - shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "installation of gcp-gateway and sinusoid packages failed" - - try: - subprocess.run(["python3 -m pip install google-cloud-pubsub==1.1.0"], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "pip installation of google-cloud-pubsub failed" - - try: - subprocess.run(["python3 -m pip install google-cloud-logging==1.15.1"], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "pip installation of google-cloud-logging failed" - - try: - subprocess.run(["if [ ! -f \"{}/roots.pem\" ]; then wget https://pki.goog/roots.pem -P {}; fi" - .format(CERTS_DIR, CERTS_DIR)], shell=True, check=True) - except subprocess.CalledProcessError: - assert False, "download of roots.pem failed" - - -def get_ping_status(fledge_url): - _connection = http.client.HTTPConnection(fledge_url) - _connection.request("GET", '/fledge/ping') - r = _connection.getresponse() - assert 200 == r.status - r = r.read().decode() - jdoc = json.loads(r) - return jdoc - - -def get_statistics_map(fledge_url): - _connection = http.client.HTTPConnection(fledge_url) - _connection.request("GET", '/fledge/statistics') - r = _connection.getresponse() - assert 200 == r.status - r = r.read().decode() - jdoc = json.loads(r) - return utils.serialize_stats_map(jdoc) - - -# Get the latest 5 timestamps, readings of data sent from south to compare it with the timestamps, -# readings of data in GCP. -def get_asset_info(fledge_url): - _connection = http.client.HTTPConnection(fledge_url) - _connection.request("GET", '/fledge/asset/sinusoid?limit=5') - r = _connection.getresponse() - assert 200 == r.status - r = r.read().decode() - jdoc = json.loads(r) - for j in jdoc: - j['timestamp'] = datetime.strptime(j['timestamp'], "%Y-%m-%d %H:%M:%S.%f").astimezone( - timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f") - return jdoc - - -def copy_certs(gcp_cert_path): - # As we are not uploading pem certificate via cert Upload API, therefore below code is required for pem certs - create_cert_pem_dir = "mkdir -p {}".format(FLEDGE_CERTS_PEM_DIR) - os.system(create_cert_pem_dir) - assert os.path.isdir(FLEDGE_CERTS_PEM_DIR) - copy_file = "cp {} {}/roots.pem {}".format(gcp_cert_path, CERTS_DIR, FLEDGE_CERTS_PEM_DIR) - os.system(copy_file) - assert os.path.isfile("{}/roots.pem".format(FLEDGE_CERTS_PEM_DIR)) - - -@pytest.fixture -def verify_and_set_prerequisites(gcp_cert_path, google_app_credentials): - assert os.path.exists("{}".format(gcp_cert_path)), "Private key not found at {}"\ - .format(gcp_cert_path) - os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = google_app_credentials - - -def verify_received_messages(logger_name, asset_info, retries, wait_time): - - from google.cloud import logging - from google.cloud.logging import DESCENDING - - # Lists the most recent entries for a given logger. - logging_client = logging.Client() - logger = logging_client.logger(logger_name) - - # Fetches the latest logs from GCP and comaperes it with current timestamp - while retries: - iterator = logger.list_entries(order_by=DESCENDING, page_size=10, filter_="severity=INFO") - pages = iterator.pages - page = next(pages) # API call - gcp_log_string = "" - gcp_info = [] - for entry in page: - gcp_log_string += entry.payload - assert len(gcp_log_string), "No data seen in GCP. " - gcp_log_dict = json.loads("[" + gcp_log_string.replace("}{", "},{") + "]") - for r in gcp_log_dict: - for d in range(0, len(r["sinusoid"])): - gcp_info.append(r["sinusoid"][d]) - assert len(gcp_info), "No Sinusoid readings GCP logs found" - found = 0 - for i in gcp_info: - for d in asset_info: - if d['timestamp'] == i['ts']: - assert d['reading']['sinusoid'] == i['sinusoid'] - found += 1 - if found == len(asset_info): - break - else: - retries -= 1 - time.sleep(wait_time) - - if retries == 0: - assert False, "TIMEOUT! sinusoid data sent not seen in GCP. " - - -class TestGCPGateway: - def test_gcp_gateway(self, check_fledge_root, verify_and_set_prerequisites, remove_and_add_pkgs, reset_fledge, - fledge_url, wait_time, remove_data_file, gcp_project_id, gcp_device_gateway_id, - gcp_registry_id, gcp_cert_path, gcp_logger_name, retries): - payload = {"name": "Sine", "type": "south", "plugin": "sinusoid", "enabled": True, "config": {}} - post_url = "/fledge/service" - conn = http.client.HTTPConnection(fledge_url) - conn.request("POST", post_url, json.dumps(payload)) - res = conn.getresponse() - assert 200 == res.status, "ERROR! POST {} request failed".format(post_url) - - copy_certs(gcp_cert_path) - - gcp_project_cfg = {"project_id": {"value": "{}".format(gcp_project_id)}, - "registry_id": {"value": "{}".format(gcp_registry_id)}, - "device_id": {"value": "{}".format(gcp_device_gateway_id)}, - "key": {"value": "rsa_private"}} - - payload = {"name": task_name, - "plugin": "{}".format(north_plugin), - "type": "north", - "schedule_type": 3, - "schedule_repeat": 5, - "schedule_enabled": True, - "config": gcp_project_cfg - } - - post_url = "/fledge/scheduled/task" - conn = http.client.HTTPConnection(fledge_url) - conn.request("POST", post_url, json.dumps(payload)) - res = conn.getresponse() - assert 200 == res.status, "ERROR! POST {} request failed".format(post_url) - - time.sleep(wait_time) - - ping_response = get_ping_status(fledge_url) - assert 0 < ping_response["dataRead"] - assert 0 < ping_response["dataSent"] - - actual_stats_map = get_statistics_map(fledge_url) - assert 0 < actual_stats_map['SINUSOID'] - assert 0 < actual_stats_map['READINGS'] - assert 0 < actual_stats_map['Readings Sent'] - assert 0 < actual_stats_map[task_name] - - asset_info = get_asset_info(fledge_url) - - verify_received_messages(gcp_logger_name, asset_info, retries, wait_time) - - remove_data_file("{}/rsa_private.pem".format(FLEDGE_CERTS_PEM_DIR)) - remove_data_file("{}/roots.pem".format(FLEDGE_CERTS_PEM_DIR)) diff --git a/tests/unit/python/fledge/services/core/api/test_package_log.py b/tests/unit/python/fledge/services/core/api/test_package_log.py index 1bdf93a4d2..0051f2e0d0 100644 --- a/tests/unit/python/fledge/services/core/api/test_package_log.py +++ b/tests/unit/python/fledge/services/core/api/test_package_log.py @@ -245,9 +245,9 @@ async def mock_coro(): return {"rows": [{'id': 'b57fd5c5-8079-49ff-b6a1-9515cbd259e4', 'name': 'fledge-south-random', 'action': "install", 'status': -1, 'log_file_uri': 'log/201006-17-02-53-fledge-south-random-install.log'}, - {'id': '1cd38675-fea8-4783-b3b5-463ed6c8cbe8', 'name': 'fledge-north-gcp', + {'id': '1cd38675-fea8-4783-b3b5-463ed6c8cbe8', 'name': 'fledge-north-kafka', 'action': "install", 'status': 0, - 'log_file_uri': 'log/201007-01-02-53-fledge-north-gcp-install.log'}, + 'log_file_uri': 'log/201007-01-02-53-fledge-north-kafka-install.log'}, {'id': '63f3c84b-0cbf-4c76-b9bf-848779fbcc6f', 'name': 'fledge-filter-fft', 'action': "update", 'status': 127, 'log_file_uri': 'log/201006-12-02-12-fledge-filter-fft-update.log'}, From d983889df35b3228bc779bac24d1672101a28b33 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Mon, 11 Dec 2023 11:42:04 +0000 Subject: [PATCH 37/54] FOGL-8324 Update column comparision to ignore integet column size (#1238) Signed-off-by: Mark Riddoch Co-authored-by: Ashish Jabble --- C/plugins/storage/postgres/connection.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/C/plugins/storage/postgres/connection.cpp b/C/plugins/storage/postgres/connection.cpp index e9e2cba18e..6b6581a738 100644 --- a/C/plugins/storage/postgres/connection.cpp +++ b/C/plugins/storage/postgres/connection.cpp @@ -4369,9 +4369,18 @@ int Connection::create_schema(const std::string &payload) { auto itr = dbCol->find(v); //Check if the column matches exactly with that present in db , if not same , the reject the request - if ( itr->type != v.type || itr->sz != v.sz || itr->key != v.key ) + // We ignore size for integer columns + if (v.type.compare("integer") == 0) { - raiseError("create_schema", "%s:%d Schema:%s, Service:%s, tableName:%s, altering an existing column %s is not allowed", __FUNCTION__, __LINE__, schema.c_str(), service.c_str(), name.c_str(), v.column.c_str() ); + if (itr->type != v.type || itr->key != v.key) + { + raiseError("create_schema", "%s:%d Schema:%s, Service:%s, tableName:%s, altering an existing column %s is not allowed", __FUNCTION__, __LINE__, schema.c_str(), service.c_str(), name.c_str(), v.column.c_str() ); + return -1; + } + } + else if (itr->type != v.type || itr->sz != v.sz || itr->key != v.key) + { + raiseError("create_schema", "%s:%d Schema:%s, Service:%s, tableName:%s, altering an existing column %s is not allowed", __FUNCTION__, __LINE__, schema.c_str(), service.c_str(), name.c_str(), v.column.c_str() ); return -1; } } From 856a67ec0e3856433c38a264e77582d80098ea2d Mon Sep 17 00:00:00 2001 From: pintomax Date: Tue, 12 Dec 2023 15:46:39 +0100 Subject: [PATCH 38/54] FOGL-8321: Fix for large value of reading id (#1244) FOGL-8321: Fix for large value of reading id --- C/common/reading_set.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/C/common/reading_set.cpp b/C/common/reading_set.cpp index 382d321bc4..df01437359 100755 --- a/C/common/reading_set.cpp +++ b/C/common/reading_set.cpp @@ -383,7 +383,7 @@ JSONReading::JSONReading(const Value& json) { if (json.HasMember("id")) { - m_id = json["id"].GetUint(); + m_id = json["id"].GetUint64(); m_has_id = true; } else From 509322936372abdf9ce5fc88623f618e68eaae2e Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Wed, 13 Dec 2023 12:50:04 +0530 Subject: [PATCH 39/54] control api invocation categorization into sub sections Signed-off-by: ashish-jabble --- docs/control.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/control.rst b/docs/control.rst index a0e3be0a1a..f52c4ab997 100644 --- a/docs/control.rst +++ b/docs/control.rst @@ -336,7 +336,7 @@ The dispatcher can also be instructed to run a local automation script, these ar This is an example and does not mean that all or any plugins will use the exact syntax for mapping described above, the documentation for your particular plugin should be consulted to confirm the mapping implemented by the plugin. API Control Invocation ----------------------- +====================== Fledge allows the administer of the system to extend to REST API of Fledge to encompass custom defined entry point for invoking control operations within the Fledge instance. These configured API Control entry points can be called with a PUT operations to a URL of the form @@ -352,7 +352,7 @@ A payload can be passed as a JSON document that may be processed into the reques This effectively adds a new entry point to the Fledge public API, calling this entry point will call the control dispatcher to effectively route a control operation from the public API to one or more south services. The definition of the Control API Entry point allows restrictions to be placed on what calls can be made, by whom and with what data. Defining API Control Entry Points -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- A control entry point has the following attributes @@ -412,7 +412,8 @@ The payload of the request is defined by the set of variables that was created w The payload sent to the dispatcher will always contain all of the variables and constants defined in the API entry point. The values for the constants are always from the original definition, whereas the values of the variables can be given in the public API or if omitted the defaults defined when the entry point was defined will be used. -Alternatively new entry points can be created using the Fledge Graphical User Interface. +Graphical User Interface +------------------------ The GUI functionality is accessed via the *API Entry Points* sub-menu of the *Control* menu in the left-hand menu pane. Selecting this option will display a screen that appears as follows. @@ -420,7 +421,11 @@ The GUI functionality is accessed via the *API Entry Points* sub-menu of the *Co | |control_api_1| | +-----------------+ -Clicking on the *Add* item in the top right corner will allow a new entry point to be defined. +Adding A Control API Entry Point +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Clicking on the *Add +* item in the top right corner will allow a new entry point to be defined. +-----------------+ | |control_api_2| | From 3d753c0e3b93efb320aa1ae093cbd14651474999 Mon Sep 17 00:00:00 2001 From: pintomax Date: Fri, 15 Dec 2023 09:45:35 +0100 Subject: [PATCH 40/54] FOGL-8323: addition of join in Postgres and sqlitelb storage plugins (#1243) FOGL-8323: addition of join to Postgres and Sqlitelb storage plugins --- C/plugins/storage/postgres/connection.cpp | 170 ++++++-- .../storage/postgres/include/connection.h | 23 +- C/plugins/storage/postgres/plugin.cpp | 2 +- .../storage/sqlitelb/common/connection.cpp | 379 +++++++++++++++++- .../sqlitelb/common/include/connection.h | 12 +- .../storage/sqlitelb/common/readings.cpp | 7 +- 6 files changed, 539 insertions(+), 54 deletions(-) diff --git a/C/plugins/storage/postgres/connection.cpp b/C/plugins/storage/postgres/connection.cpp index 6b6581a738..357cb5a008 100644 --- a/C/plugins/storage/postgres/connection.cpp +++ b/C/plugins/storage/postgres/connection.cpp @@ -247,7 +247,9 @@ bool Connection::aggregateQuery(const Value& payload, string& resultSet) // Add where condition sql.append("WHERE "); - if (!jsonWhereClause(payload["where"], sql)) + + vector asset_codes; + if (!jsonWhereClause(payload["where"], sql, asset_codes)) { raiseError("retrieve", "aggregateQuery: failure while building WHERE clause"); return false; @@ -375,11 +377,15 @@ Connection::~Connection() * Perform a query against a common table * */ -bool Connection::retrieve(const string& table, const string& condition, string& resultSet) +bool Connection::retrieve(const string& schema, + const string& table, + const string& condition, + string& resultSet) { Document document; // Default template parameter uses UTF8 and MemoryPoolAllocator. SQLBuffer sql; SQLBuffer jsonConstraints; // Extra constraints to add to where clause +vector asset_codes; try { if (condition.empty()) @@ -408,6 +414,11 @@ SQLBuffer jsonConstraints; // Extra constraints to add to where clause } sql.append(" FROM "); } + else if (document.HasMember("join")) + { + sql.append("SELECT "); + selectColumns(document, sql, 0); + } else if (document.HasMember("return")) { int col = 0; @@ -512,14 +523,70 @@ SQLBuffer jsonConstraints; // Extra constraints to add to where clause } sql.append(" * FROM "); } - sql.append(table); + + if (document.HasMember("join")) + { + sql.append(" FROM "); + sql.append(table); + sql.append(" t0"); + appendTables(schema, document, sql, 1); + } + else + { + sql.append(table); + } if (document.HasMember("where")) { sql.append(" WHERE "); - - if (document.HasMember("where")) + + if (document.HasMember("join")) + { + if (!jsonWhereClause(document["where"], sql, asset_codes, false, "t0.")) + { + return false; + } + + // Now and the join condition itself + string col0, col1; + const Value& join = document["join"]; + if (join.HasMember("on") && join["on"].IsString()) + { + col0 = join["on"].GetString(); + } + else + { + + raiseError("rerieve", "Missing on item"); + return false; + } + if (join.HasMember("table")) + { + const Value& table = join["table"]; + if (table.HasMember("column") && table["column"].IsString()) + { + col1 = table["column"].GetString(); + } + else + { + raiseError("QueryTable", "Missing column in join table"); + return false; + } + } + sql.append(" AND t0."); + sql.append(col0); + sql.append(" = t1."); + sql.append(col1); + sql.append(" "); + if (join.HasMember("query") && join["query"].IsObject()) + { + sql.append("AND "); + const Value& query = join["query"]; + processJoinQueryWhereClause(query, sql, asset_codes, 1); + } + } + else if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql)) + if (!jsonWhereClause(document["where"], sql, asset_codes)) { return false; } @@ -783,7 +850,8 @@ bool Connection::retrieveReadings(const string& condition, string& resultSet) if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql)) + vector asset_codes; + if (!jsonWhereClause(document["where"], sql, asset_codes)) { return false; } @@ -1279,15 +1347,17 @@ SQLBuffer sql; if ((*iter).HasMember("condition")) { sql.append(" WHERE "); - if (!jsonWhereClause((*iter)["condition"], sql)) + vector asset_codes; + if (!jsonWhereClause((*iter)["condition"], sql, asset_codes)) { return false; } } else if ((*iter).HasMember("where")) { + vector asset_codes; sql.append(" WHERE "); - if (!jsonWhereClause((*iter)["where"], sql)) + if (!jsonWhereClause((*iter)["where"], sql, asset_codes)) { return false; } @@ -1354,7 +1424,8 @@ SQLBuffer sql; { if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql)) + vector asset_codes; + if (!jsonWhereClause(document["where"], sql, asset_codes)) { return -1; } @@ -2843,8 +2914,11 @@ bool Connection::jsonModifiers(const Value& payload, SQLBuffer& sql) */ bool Connection::jsonWhereClause(const Value& whereClause, SQLBuffer& sql, - const string& prefix) + vector &asset_codes, + bool convertLocaltime, // not in use + const string prefix) { + if (!whereClause.IsObject()) { raiseError("where clause", "The \"where\" property must be a JSON object"); @@ -2867,17 +2941,31 @@ bool Connection::jsonWhereClause(const Value& whereClause, double converted = strtod(whereColumnName.c_str(), &p); if (*p) { - // Quote column name - sql.append("\""); + // Double quote column name + if (prefix.empty()) + { + sql.append("\""); + } + + // Add prefix if (!prefix.empty()) + { sql.append(prefix); - sql.append(whereClause["column"].GetString()); - sql.append("\""); + + } + + sql.append(whereColumnName); + + // Double quote column name + if (prefix.empty()) + { + sql.append("\""); + } } else { - // Use converted numeric value - sql.append(whereClause["column"].GetString()); + // Use numeric value + sql.append(whereColumnName); } sql.append(' '); @@ -2990,8 +3078,18 @@ bool Connection::jsonWhereClause(const Value& whereClause, } else if (whereClause["value"].IsString()) { sql.append('\''); - sql.append(escape(whereClause["value"].GetString())); + string value = whereClause["value"].GetString(); + sql.append(escape(value)); sql.append('\''); + + // Identify a specific operation to restrinct the tables involved + if (whereColumnName.compare("asset_code") == 0) + { + if ( cond.compare("=") == 0) + { + asset_codes.push_back(value); + } + } } } } @@ -2999,15 +3097,17 @@ bool Connection::jsonWhereClause(const Value& whereClause, if (whereClause.HasMember("and")) { sql.append(" AND "); - if (!jsonWhereClause(whereClause["and"], sql)) + vector asset_codes; + if (!jsonWhereClause(whereClause["and"], sql, asset_codes, false, prefix)) { return false; } } if (whereClause.HasMember("or")) { + vector asset_codes; sql.append(" OR "); - if (!jsonWhereClause(whereClause["or"], sql)) + if (!jsonWhereClause(whereClause["or"], sql, asset_codes, false, prefix)) { return false; } @@ -3552,7 +3652,10 @@ SQLBuffer jsonConstraints; * @param sql The SQLBuffer we are writing * @param level The table number we are processing */ -bool Connection::appendTables(const Value& document, SQLBuffer& sql, int level) +bool Connection::appendTables(const string& schema, + const Value& document, + SQLBuffer& sql, + int level) { string tag = "t" + to_string(level); if (document.HasMember("join")) @@ -3572,14 +3675,17 @@ bool Connection::appendTables(const Value& document, SQLBuffer& sql, int level) raiseError("commonRetrieve", "Joining table name is not a string"); return false; } - sql.append(", fledge."); - sql.append(name.GetString()); - sql.append(" "); - sql.append(tag); + + sql.append(", "); + sql.append(schema); + sql.append('.'); + sql.append(name.GetString()); + sql.append(" "); + sql.append(tag); if (join.HasMember("query")) { const Value& query = join["query"]; - appendTables(query, sql, ++level); + appendTables(schema, query, sql, ++level); } else { @@ -3602,12 +3708,16 @@ bool Connection::appendTables(const Value& document, SQLBuffer& sql, int level) * * @param query The JSON query * @param sql The SQLBuffer we are writing the data to - * @param level The nestign level of the joined table + * @param asset_codes The asset codes + * @param level The nesting level of the joined table */ -bool Connection::processJoinQueryWhereClause(const Value& query, SQLBuffer& sql, int level) +bool Connection::processJoinQueryWhereClause(const Value& query, + SQLBuffer& sql, + std::vector &asset_codes, + int level) { string tag = "t" + to_string(level) + "."; - if (!jsonWhereClause(query["where"], sql, tag)) + if (!jsonWhereClause(query["where"], sql, asset_codes, false, tag)) { return false; } @@ -3650,7 +3760,7 @@ bool Connection::processJoinQueryWhereClause(const Value& query, SQLBuffer& sql, { sql.append(" AND "); const Value& query = join["query"]; - processJoinQueryWhereClause(query, sql, level + 1); + processJoinQueryWhereClause(query, sql, asset_codes, level + 1); } } return true; diff --git a/C/plugins/storage/postgres/include/connection.h b/C/plugins/storage/postgres/include/connection.h index 1a99ed9b2c..d9ad109258 100644 --- a/C/plugins/storage/postgres/include/connection.h +++ b/C/plugins/storage/postgres/include/connection.h @@ -33,7 +33,8 @@ class Connection { public: Connection(); ~Connection(); - bool retrieve(const std::string& table, const std::string& condition, + bool retrieve(const std::string& schema, + const std::string& table, const std::string& condition, std::string& resultSet); bool retrieveReadings(const std::string& condition, std::string& resultSet); int insert(const std::string& table, const std::string& data); @@ -69,9 +70,15 @@ class Connection { void raiseError(const char *operation, const char *reason,...); PGconn *dbConnection; void mapResultSet(PGresult *res, std::string& resultSet); - bool jsonWhereClause(const rapidjson::Value& whereClause, SQLBuffer&, const std::string& prefix = ""); bool jsonModifiers(const rapidjson::Value&, SQLBuffer&); - bool jsonAggregates(const rapidjson::Value&, const rapidjson::Value&, SQLBuffer&, SQLBuffer&, bool isTableReading = false); + bool jsonAggregates(const rapidjson::Value&, + const rapidjson::Value&, + SQLBuffer&, SQLBuffer&, + bool isTableReading = false); + bool jsonWhereClause(const rapidjson::Value& whereClause, + SQLBuffer&, std::vector &asset_codes, + bool convertLocaltime = false, + std::string prefix = ""); bool returnJson(const rapidjson::Value&, SQLBuffer&, SQLBuffer&); char *trim(char *str); const std::string escape_double_quotes(const std::string&); @@ -80,8 +87,14 @@ class Connection { void logSQL(const char *, const char *); bool isFunction(const char *) const; bool selectColumns(const rapidjson::Value& document, SQLBuffer& sql, int level); - bool appendTables(const rapidjson::Value& document, SQLBuffer& sql, int level); - bool processJoinQueryWhereClause(const rapidjson::Value& query, SQLBuffer& sql, int level); + bool appendTables(const std::string &schema, + const rapidjson::Value& document, + SQLBuffer& sql, + int level); + bool processJoinQueryWhereClause(const rapidjson::Value& query, + SQLBuffer& sql, + std::vector &asset_codes, + int level); std::string getIndexName(std::string s); bool checkValidDataType(const std::string &s); diff --git a/C/plugins/storage/postgres/plugin.cpp b/C/plugins/storage/postgres/plugin.cpp index bdd0902b43..3892a7b44f 100644 --- a/C/plugins/storage/postgres/plugin.cpp +++ b/C/plugins/storage/postgres/plugin.cpp @@ -122,7 +122,7 @@ ConnectionManager *manager = (ConnectionManager *)handle; Connection *connection = manager->allocate(); std::string results; - bool rval = connection->retrieve(std::string(OR_DEFAULT_SCHEMA(schema)) + "." + std::string(table), std::string(query), results); + bool rval = connection->retrieve(schema, std::string(OR_DEFAULT_SCHEMA(schema)) + "." + std::string(table), std::string(query), results); manager->release(connection); if (rval) { diff --git a/C/plugins/storage/sqlitelb/common/connection.cpp b/C/plugins/storage/sqlitelb/common/connection.cpp index f4b5f8b9b0..d271811078 100644 --- a/C/plugins/storage/sqlitelb/common/connection.cpp +++ b/C/plugins/storage/sqlitelb/common/connection.cpp @@ -1021,6 +1021,7 @@ Document document; SQLBuffer sql; // Extra constraints to add to where clause SQLBuffer jsonConstraints; +vector asset_codes; if (!m_schemaManager->exists(dbHandle, schema)) { @@ -1064,7 +1065,14 @@ SQLBuffer jsonConstraints; { return false; } - sql.append(" FROM fledge."); + sql.append(" FROM "); + sql.append(schema); + sql.append('.'); + } + else if (document.HasMember("join")) + { + sql.append("SELECT "); + selectColumns(document, sql, 0); } else if (document.HasMember("return")) { @@ -1167,7 +1175,9 @@ SQLBuffer jsonConstraints; } col++; } - sql.append(" FROM fledge."); + sql.append(" FROM "); + sql.append(schema); + sql.append('.'); } else { @@ -1177,17 +1187,76 @@ SQLBuffer jsonConstraints; sql.append(document["modifier"].GetString()); sql.append(' '); } - sql.append(" * FROM fledge."); + sql.append(" * FROM "); + sql.append(schema); + sql.append('.'); + } + if (document.HasMember("join")) + { + sql.append(" FROM "); + sql.append(schema); + sql.append('.'); + sql.append(table); + sql.append(" t0"); + appendTables(schema, document, sql, 1); + } + else + { + sql.append(table); } - sql.append(table); if (document.HasMember("where")) { sql.append(" WHERE "); - - if (document.HasMember("where")) + + if (document.HasMember("join")) + { + if (!jsonWhereClause(document["where"], sql, asset_codes, false, "t0.")) + { + return false; + } + + // Now and the join condition itself + string col0, col1; + const Value& join = document["join"]; + if (join.HasMember("on") && join["on"].IsString()) + { + col0 = join["on"].GetString(); + } + else + { + raiseError("rerieve", "Missing on item"); + return false; + } + if (join.HasMember("table")) + { + const Value& table = join["table"]; + if (table.HasMember("column") && table["column"].IsString()) + { + col1 = table["column"].GetString(); + } + else + { + raiseError("QueryTable", "Missing column in join table"); + return false; + } + } + sql.append(" AND t0."); + sql.append(col0); + sql.append(" = t1."); + sql.append(col1); + sql.append(" "); + if (join.HasMember("query") && join["query"].IsObject()) + { + sql.append("AND "); + const Value& query = join["query"]; + processJoinQueryWhereClause(query, sql, asset_codes, 1); + } + } + else if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql, true)) + if (!jsonWhereClause(document["where"], sql, asset_codes, false)) { + raiseError("retrieve", "Failed to add where clause"); return false; } } @@ -1485,6 +1554,7 @@ int Connection::update(const string& schema, Document document; SQLBuffer sql; bool allowZero = false; +vector asset_codes; int row = 0; ostringstream convert; @@ -1806,7 +1876,7 @@ bool allowZero = false; if ((*iter).HasMember("condition")) { sql.append(" WHERE "); - if (!jsonWhereClause((*iter)["condition"], sql)) + if (!jsonWhereClause((*iter)["condition"], sql, asset_codes)) { return false; } @@ -1814,7 +1884,7 @@ bool allowZero = false; else if ((*iter).HasMember("where")) { sql.append(" WHERE "); - if (!jsonWhereClause((*iter)["where"], sql)) + if (!jsonWhereClause((*iter)["where"], sql, asset_codes)) { return false; } @@ -2720,7 +2790,9 @@ bool Connection::jsonModifiers(const Value& payload, */ bool Connection::jsonWhereClause(const Value& whereClause, SQLBuffer& sql, - bool convertLocaltime) + std::vector &asset_codes, + bool convertLocaltime, + string prefix) { if (!whereClause.IsObject()) { @@ -2738,7 +2810,12 @@ bool Connection::jsonWhereClause(const Value& whereClause, return false; } - sql.append(whereClause["column"].GetString()); + string column = whereClause["column"].GetString(); + if (!prefix.empty()) + { + sql.append(prefix); + } + sql.append(column); sql.append(' '); string cond = whereClause["condition"].GetString(); @@ -2858,9 +2935,15 @@ bool Connection::jsonWhereClause(const Value& whereClause, sql.append(whereClause["value"].GetInt()); } else if (whereClause["value"].IsString()) { + string value = whereClause["value"].GetString(); sql.append('\''); - sql.append(escape(whereClause["value"].GetString())); + sql.append(escape(value)); sql.append('\''); + + // Identify a specific operation to restrinct the tables involved + if (column.compare("asset_code") == 0) + if ( cond.compare("=") == 0) + asset_codes.push_back(value); } } } @@ -2868,7 +2951,7 @@ bool Connection::jsonWhereClause(const Value& whereClause, if (whereClause.HasMember("and")) { sql.append(" AND "); - if (!jsonWhereClause(whereClause["and"], sql, convertLocaltime)) + if (!jsonWhereClause(whereClause["and"], sql, asset_codes, convertLocaltime, prefix)) { return false; } @@ -2876,7 +2959,7 @@ bool Connection::jsonWhereClause(const Value& whereClause, if (whereClause.HasMember("or")) { sql.append(" OR "); - if (!jsonWhereClause(whereClause["or"], sql, convertLocaltime)) + if (!jsonWhereClause(whereClause["or"], sql, asset_codes, convertLocaltime, prefix)) { return false; } @@ -3258,6 +3341,7 @@ int Connection::deleteRows(const string& schema, // Default template parameter uses UTF8 and MemoryPoolAllocator. Document document; SQLBuffer sql; +vector asset_codes; if (!m_schemaManager->exists(dbHandle, schema)) { @@ -3285,7 +3369,7 @@ SQLBuffer sql; { if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql)) + if (!jsonWhereClause(document["where"], sql, asset_codes)) { return -1; } @@ -3581,3 +3665,268 @@ string Connection::operation(const char *sql) return string(buf); } + +/** + * In the case of a join add the tables to select from for all the tables in + * the join + * + * @param schema The schema we are using + * @param document The query we are processing + * @param sql The SQLBuffer we are writing + * @param level The table number we are processing + */ +bool Connection::appendTables(const string& schema, const Value& document, SQLBuffer& sql, int level) +{ + string tag = "t" + to_string(level); + if (document.HasMember("join")) + { + const Value& join = document["join"]; + if (join.HasMember("table")) + { + const Value& table = join["table"]; + if (!table.HasMember("name")) + { + raiseError("commonRetrieve", "Joining table is missing a table name"); + return false; + } + const Value& name = table["name"]; + if (!name.IsString()) + { + raiseError("commonRetrieve", "Joining table name is not a string"); + return false; + } + sql.append(", "); + sql.append(schema); + sql.append('.'); + sql.append(name.GetString()); + sql.append(" "); + sql.append(tag); + if (join.HasMember("query")) + { + const Value& query = join["query"]; + appendTables(schema, query, sql, ++level); + } + else + { + raiseError("commonRetrieve", "Join is missing a join query definition"); + return false; + } + } + else + { + raiseError("commonRetrieve", "Join is missing a table definition"); + return false; + } + } + return true; +} + +/** + * Recurse down and add the where cluase and join terms for each + * new table joined to the query + * + * @param query The JSON query + * @param sql The SQLBuffer we are writing the data to + * @param asset_codes The asset codes + * @param level The nestign level of the joined table + */ +bool Connection::processJoinQueryWhereClause(const Value& query, + SQLBuffer& sql, + std::vector &asset_codes, + int level) +{ + string tag = "t" + to_string(level) + "."; + if (!jsonWhereClause(query["where"], sql, asset_codes, true, tag)) + { + return false; + } + + if (query.HasMember("join")) + { + // Now and the join condition itself + string col0, col1; + const Value& join = query["join"]; + if (join.HasMember("on") && join["on"].IsString()) + { + col0 = join["on"].GetString(); + } + else + { + return false; + } + if (join.HasMember("table")) + { + const Value& table = join["table"]; + if (table.HasMember("column") && table["column"].IsString()) + { + col1 = table["column"].GetString(); + } + else + { + raiseError("Joined query", "Missing join column in table"); + return false; + } + } + sql.append(" AND "); + sql.append(tag); + sql.append(col0); + sql.append(" = t"); + sql.append(level + 1); + sql.append("."); + sql.append(col1); + sql.append(" "); + if (join.HasMember("query") && join["query"].IsObject()) + { + sql.append(" AND "); + const Value& query = join["query"]; + processJoinQueryWhereClause(query, sql, asset_codes, level + 1); + } + } + return true; +} + +/** + * In the case of a join add the columns to select from for all the tables in + * the join + * + * @param document The query we are processing + * @param sql The SQLBuffer we are writing + * @param level The table number we are processing + */ +bool Connection::selectColumns(const Value& document, SQLBuffer& sql, int level) +{ +SQLBuffer jsonConstraints; + +string tag = "t" + to_string(level) + "."; + + if (document.HasMember("return")) + { + int col = 0; + const Value& columns = document["return"]; + if (! columns.IsArray()) + { + raiseError("retrieve", "The property return must be an array"); + return false; + } + if (document.HasMember("modifier")) + { + sql.append(document["modifier"].GetString()); + sql.append(' '); + } + for (Value::ConstValueIterator itr = columns.Begin(); itr != columns.End(); ++itr) + { + if (col) + { + sql.append(", "); + } + if (!itr->IsObject()) // Simple column name + { + sql.append(tag); + sql.append(itr->GetString()); + } + else + { + if (itr->HasMember("column")) + { + if (! (*itr)["column"].IsString()) + { + raiseError("rerieve", + "column must be a string"); + return false; + } + if (itr->HasMember("format")) + { + if (! (*itr)["format"].IsString()) + { + raiseError("rerieve", + "format must be a string"); + return false; + } + + // SQLite 3 date format. + string new_format; + applyColumnDateFormat((*itr)["format"].GetString(), + tag + (*itr)["column"].GetString(), + new_format, + true); + + // Add the formatted column or use it as is + sql.append(new_format); + } + else if (itr->HasMember("timezone")) + { + if (! (*itr)["timezone"].IsString()) + { + raiseError("rerieve", + "timezone must be a string"); + return false; + } + // SQLite3 doesnt support time zone formatting + if (strcasecmp((*itr)["timezone"].GetString(), "utc") != 0) + { + raiseError("retrieve", + "SQLite3 plugin does not support timezones in qeueries"); + return false; + } + else + { + sql.append("strftime('" F_DATEH24_MS "', "); + sql.append(tag); + sql.append((*itr)["column"].GetString()); + + sql.append(", 'utc')"); + } + } + else + { + sql.append(tag); + sql.append((*itr)["column"].GetString()); + } + sql.append(' '); + } + else if (itr->HasMember("json")) + { + const Value& json = (*itr)["json"]; + if (! returnJson(json, sql, jsonConstraints)) + { + return false; + } + } + else + { + raiseError("retrieve", + "return object must have either a column or json property"); + return false; + } + + if (itr->HasMember("alias")) + { + sql.append(" AS \""); + sql.append((*itr)["alias"].GetString()); + sql.append('"'); + } + } + col++; + } + } + else + { + sql.append('*'); + return true; + } + if (document.HasMember("join")) + { + const Value& join = document["join"]; + if (join.HasMember("query")) + { + const Value& query = join["query"]; + sql.append(", "); + if (!selectColumns(query, sql, ++level)) + { + raiseError("commonRetrieve", "Join failed to add select columns"); + return false; + } + } + } + return true; +} diff --git a/C/plugins/storage/sqlitelb/common/include/connection.h b/C/plugins/storage/sqlitelb/common/include/connection.h index 703b457b2d..ad1ac00aa4 100644 --- a/C/plugins/storage/sqlitelb/common/include/connection.h +++ b/C/plugins/storage/sqlitelb/common/include/connection.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #ifndef MEMORY_READING_PLUGIN #include @@ -140,7 +141,10 @@ class Connection { void raiseError(const char *operation, const char *reason,...); sqlite3 *dbHandle; int mapResultSet(void *res, std::string& resultSet); - bool jsonWhereClause(const rapidjson::Value& whereClause, SQLBuffer&, bool convertLocaltime = false); + bool jsonWhereClause(const rapidjson::Value& whereClause, + SQLBuffer&, std::vector &asset_codes, + bool convertLocaltime = false, + std::string prefix = ""); bool jsonModifiers(const rapidjson::Value&, SQLBuffer&, bool isTableReading = false); bool jsonAggregates(const rapidjson::Value&, const rapidjson::Value&, @@ -155,5 +159,11 @@ class Connection { int i, std::string& newDate); void logSQL(const char *, const char *); + bool appendTables(const std::string& schema, const rapidjson::Value& document, SQLBuffer& sql, int level); + bool processJoinQueryWhereClause(const rapidjson::Value& query, + SQLBuffer& sql, + std::vector &asset_codes, + int level); + bool selectColumns(const rapidjson::Value& document, SQLBuffer& sql, int level); }; #endif diff --git a/C/plugins/storage/sqlitelb/common/readings.cpp b/C/plugins/storage/sqlitelb/common/readings.cpp index b2ce491127..fb6cbad35a 100644 --- a/C/plugins/storage/sqlitelb/common/readings.cpp +++ b/C/plugins/storage/sqlitelb/common/readings.cpp @@ -149,6 +149,8 @@ bool aggregateAll(const Value& payload) */ bool Connection::aggregateQuery(const Value& payload, string& resultSet) { + vector asset_codes; + if (!payload.HasMember("where") || !payload.HasMember("timebucket")) { @@ -300,7 +302,7 @@ bool Connection::aggregateQuery(const Value& payload, string& resultSet) // Add where condition sql.append("WHERE "); - if (!jsonWhereClause(payload["where"], sql)) + if (!jsonWhereClause(payload["where"], sql, asset_codes)) { raiseError("retrieve", "aggregateQuery: failure while building WHERE clause"); return false; @@ -1268,6 +1270,7 @@ SQLBuffer sql; SQLBuffer jsonConstraints; bool isAggregate = false; const char *timezone = "utc"; +vector asset_codes; try { if (dbHandle == NULL) @@ -1545,7 +1548,7 @@ const char *timezone = "utc"; if (document.HasMember("where")) { - if (!jsonWhereClause(document["where"], sql)) + if (!jsonWhereClause(document["where"], sql, asset_codes)) { return false; } From 6c45836f123c502d9bf10e6f542ca62400118f33 Mon Sep 17 00:00:00 2001 From: Ray Verhoeff Date: Sat, 16 Dec 2023 11:46:31 -0500 Subject: [PATCH 41/54] FOGL-8264: fix Send Full Structure so only PI Points are created when false (#1247) * Add elapsed time report to omf.log Signed-off-by: Ray Verhoeff * Add elapsed time report for exceptions to omf.log Signed-off-by: Ray Verhoeff * Fix Send Full Structure so only PI Points are created when false Signed-off-by: Ray Verhoeff --------- Signed-off-by: Ray Verhoeff Co-authored-by: Mark Riddoch --- C/plugins/common/libcurl_https.cpp | 26 ++++++++++++++++++++ C/plugins/common/simple_http.cpp | 26 ++++++++++++++++++++ C/plugins/common/simple_https.cpp | 27 ++++++++++++++++++++- C/plugins/north/OMF/include/omflinkeddata.h | 3 +++ C/plugins/north/OMF/linkdata.cpp | 4 +-- C/plugins/north/OMF/omf.cpp | 1 + C/plugins/north/OMF/plugin.cpp | 4 +-- 7 files changed, 86 insertions(+), 5 deletions(-) diff --git a/C/plugins/common/libcurl_https.cpp b/C/plugins/common/libcurl_https.cpp index f1cc6eb582..3bb6eac64e 100644 --- a/C/plugins/common/libcurl_https.cpp +++ b/C/plugins/common/libcurl_https.cpp @@ -30,6 +30,21 @@ using namespace std; +/** + * Creates a UTC time string for the current time + * + * @return Current UTC time + */ +static std::string CurrentTimeString() +{ + time_t now = time(NULL); + struct tm timeinfo; + gmtime_r(&now, &timeinfo); + char timeString[20]; + strftime(timeString, sizeof(timeString), "%F %T", &timeinfo); + return std::string(timeString); +} + /** * Constructor: host:port, connect_timeout, request_timeout, * retry_sleep_Time, max_retry @@ -312,6 +327,7 @@ int LibcurlHttps::sendRequest( do { + std::chrono::high_resolution_clock::time_point tStart; try { exceptionRaised = none; @@ -334,6 +350,7 @@ int LibcurlHttps::sendRequest( } m_ofs << "Payload:" << endl; m_ofs << payload << endl; + tStart = std::chrono::high_resolution_clock::now(); } // Execute the HTTP method @@ -346,8 +363,10 @@ int LibcurlHttps::sendRequest( httpResponseText = httpHeaderBuffer; if (m_log) { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); m_ofs << "Response:" << endl; m_ofs << " Code: " << httpCode << endl; + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; m_ofs << " Content: " << httpResponseText << endl << endl; } StringStripCRLF(httpResponseText); @@ -409,6 +428,13 @@ int LibcurlHttps::sendRequest( } #endif + if (m_log && !errorMessage.empty()) + { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; + m_ofs << " Exception: " << errorMessage << endl; + } + if (retryCount < m_max_retry) { this_thread::sleep_for(chrono::seconds(sleepTime)); diff --git a/C/plugins/common/simple_http.cpp b/C/plugins/common/simple_http.cpp index 4c76a0c28c..020cc3e448 100644 --- a/C/plugins/common/simple_http.cpp +++ b/C/plugins/common/simple_http.cpp @@ -17,6 +17,21 @@ using namespace std; +/** + * Creates a UTC time string for the current time + * + * @return Current UTC time + */ +static std::string CurrentTimeString() +{ + time_t now = time(NULL); + struct tm timeinfo; + gmtime_r(&now, &timeinfo); + char timeString[20]; + strftime(timeString, sizeof(timeString), "%F %T", &timeinfo); + return std::string(timeString); +} + // Using https://github.com/eidheim/Simple-Web-Server using HttpClient = SimpleWeb::Client; @@ -126,6 +141,7 @@ int SimpleHttp::sendRequest( do { + std::chrono::high_resolution_clock::time_point tStart; try { exception_raised = none; @@ -141,6 +157,7 @@ int SimpleHttp::sendRequest( } m_ofs << "Payload:" << endl; m_ofs << payload << endl; + tStart = std::chrono::high_resolution_clock::now(); } // Call HTTPS method @@ -151,8 +168,10 @@ int SimpleHttp::sendRequest( if (m_log) { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); m_ofs << "Response:" << endl; m_ofs << " Code: " << res->status_code << endl; + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; m_ofs << " Content: " << res->content.string() << endl << endl; } @@ -213,6 +232,13 @@ int SimpleHttp::sendRequest( } #endif + if (m_log && !exception_message.empty()) + { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; + m_ofs << " Exception: " << exception_message << endl; + } + if (retry_count < m_max_retry) { this_thread::sleep_for(chrono::seconds(sleep_time)); diff --git a/C/plugins/common/simple_https.cpp b/C/plugins/common/simple_https.cpp index 35c2584c49..18a138a010 100644 --- a/C/plugins/common/simple_https.cpp +++ b/C/plugins/common/simple_https.cpp @@ -19,6 +19,21 @@ using namespace std; +/** + * Creates a UTC time string for the current time + * + * @return Current UTC time + */ +static std::string CurrentTimeString() +{ + time_t now = time(NULL); + struct tm timeinfo; + gmtime_r(&now, &timeinfo); + char timeString[20]; + strftime(timeString, sizeof(timeString), "%F %T", &timeinfo); + return std::string(timeString); +} + // Using https://github.com/eidheim/Simple-Web-Server using HttpsClient = SimpleWeb::Client; @@ -137,6 +152,7 @@ int SimpleHttps::sendRequest( do { + std::chrono::high_resolution_clock::time_point tStart; try { exception_raised = none; @@ -152,6 +168,7 @@ int SimpleHttps::sendRequest( } m_ofs << "Payload:" << endl; m_ofs << payload << endl; + tStart = std::chrono::high_resolution_clock::now(); } // Call HTTPS method @@ -163,8 +180,10 @@ int SimpleHttps::sendRequest( if (m_log) { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); m_ofs << "Response:" << endl; m_ofs << " Code: " << res->status_code << endl; + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; m_ofs << " Content: " << res->content.string() << endl << endl; } @@ -179,7 +198,6 @@ int SimpleHttps::sendRequest( { exception_raised = typeBadRequest; exception_message = ex.what(); - } catch (exception &ex) { @@ -222,6 +240,13 @@ int SimpleHttps::sendRequest( } #endif + if (m_log && !exception_message.empty()) + { + std::chrono::high_resolution_clock::time_point tEnd = std::chrono::high_resolution_clock::now(); + m_ofs << " Time: " << ((double)std::chrono::duration_cast(tEnd - tStart).count()) / 1.0E6 << " sec " << CurrentTimeString() << endl; + m_ofs << " Exception: " << exception_message << endl; + } + if (retry_count < m_max_retry) { this_thread::sleep_for(chrono::seconds(sleep_time)); diff --git a/C/plugins/north/OMF/include/omflinkeddata.h b/C/plugins/north/OMF/include/omflinkeddata.h index 9701e93e2c..c25d1ba38a 100644 --- a/C/plugins/north/OMF/include/omflinkeddata.h +++ b/C/plugins/north/OMF/include/omflinkeddata.h @@ -46,6 +46,7 @@ class OMFLinkedData const std::string& DefaultAFLocation = std::string(), OMFHints *hints = NULL); void buildLookup(const std::vector& reading); + void setSendFullStructure(const bool sendFullStructure) {m_sendFullStructure = sendFullStructure;}; bool flushContainers(HttpSender& sender, const std::string& path, std::vector >& header); void setFormats(const std::string& doubleFormat, const std::string& integerFormat) { @@ -69,6 +70,8 @@ class OMFLinkedData }; private: + bool m_sendFullStructure; + /** * The container for this asset and data point has been sent in * this session. The key is the asset followed by the datapoint name diff --git a/C/plugins/north/OMF/linkdata.cpp b/C/plugins/north/OMF/linkdata.cpp index ce1a61bdb3..af5f6fe9ad 100644 --- a/C/plugins/north/OMF/linkdata.cpp +++ b/C/plugins/north/OMF/linkdata.cpp @@ -116,7 +116,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi Logger::getLogger()->fatal("FIXME: no asset lookup item for %s.", assetName.c_str()); return ""; } - if (assetLookup->second.assetState() == false) + if (m_sendFullStructure && assetLookup->second.assetState() == false) { // Send the data message to create the asset instance outData.append("{ \"typeid\":\"FledgeAsset\", \"values\":[ { \"AssetId\":\""); @@ -219,7 +219,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi skippedDatapoints.push_back(dpName); continue; } - if (dpLookup->second.linkState() == false) + if (m_sendFullStructure && dpLookup->second.linkState() == false) { outData.append("{ \"typeid\":\"__Link\","); outData.append("\"values\":[ { \"source\" : {"); diff --git a/C/plugins/north/OMF/omf.cpp b/C/plugins/north/OMF/omf.cpp index 53443787e0..53c1e7c4a9 100755 --- a/C/plugins/north/OMF/omf.cpp +++ b/C/plugins/north/OMF/omf.cpp @@ -1163,6 +1163,7 @@ uint32_t OMF::sendToServer(const vector& readings, // Create the class that deals with the linked data generation OMFLinkedData linkedData(&m_linkedAssetState, m_PIServerEndpoint); + linkedData.setSendFullStructure(m_sendFullStructure); linkedData.setFormats(getFormatType(OMF_TYPE_FLOAT), getFormatType(OMF_TYPE_INTEGER)); // Create the lookup data for this block of readings diff --git a/C/plugins/north/OMF/plugin.cpp b/C/plugins/north/OMF/plugin.cpp index 6d2d543c71..3708476ebe 100755 --- a/C/plugins/north/OMF/plugin.cpp +++ b/C/plugins/north/OMF/plugin.cpp @@ -107,11 +107,11 @@ const char *PLUGIN_DEFAULT_CONFIG_INFO = QUOTE( "validity" : "PIServerEndpoint == \"AVEVA Data Hub\" || PIServerEndpoint == \"OSIsoft Cloud Services\"" }, "SendFullStructure": { - "description": "It sends the minimum OMF structural messages to load data into Data Archive if disabled", + "description": "If true, create an AF structure to organize the data. If false, create PI Points only.", "type": "boolean", "default": "true", "order": "3", - "displayName": "Send full structure", + "displayName": "Create AF structure", "validity" : "PIServerEndpoint == \"PI Web API\"" }, "NamingScheme": { From 3184a1aabde2178e874bd629147e679b914ef14c Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Mon, 18 Dec 2023 15:14:32 +0530 Subject: [PATCH 42/54] allowed either variable or constant for type write in Control Pipeline Entrypoint API Signed-off-by: ashish-jabble --- .../core/api/control_service/entrypoint.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/python/fledge/services/core/api/control_service/entrypoint.py b/python/fledge/services/core/api/control_service/entrypoint.py index 1c5942a11a..13a0a216ee 100644 --- a/python/fledge/services/core/api/control_service/entrypoint.py +++ b/python/fledge/services/core/api/control_service/entrypoint.py @@ -164,23 +164,17 @@ async def _check_parameters(payload, skip_required=False): if constants is not None: if not isinstance(constants, dict): raise ValueError('constants should be a dictionary.') - if not constants and _type == EntryPointType.WRITE.name.lower(): - raise ValueError('constants should not be empty.') final['constants'] = constants - else: - if _type == EntryPointType.WRITE.name.lower(): - raise ValueError("For type write constants must have passed in payload and cannot have empty value.") variables = payload.get('variables', None) if variables is not None: if not isinstance(variables, dict): raise ValueError('variables should be a dictionary.') - if not variables and _type == EntryPointType.WRITE.name.lower(): - raise ValueError('variables should not be empty.') final['variables'] = variables - else: - if _type == EntryPointType.WRITE.name.lower(): - raise ValueError("For type write variables must have passed in payload and cannot have empty value.") + + if _type == EntryPointType.WRITE.name.lower(): + if not variables and not constants: + raise ValueError('For write type either variables or constants should not be empty.') allow = payload.get('allow', None) if allow is not None: From 7b3e441df1bebff9171511594f34dca75d766c74 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Mon, 18 Dec 2023 15:15:17 +0530 Subject: [PATCH 43/54] unit tests updated for write type in control entrypoint create or update Signed-off-by: ashish-jabble --- .../api/control_service/test_entrypoint.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/unit/python/fledge/services/core/api/control_service/test_entrypoint.py b/tests/unit/python/fledge/services/core/api/control_service/test_entrypoint.py index 13c842ffdc..95d85a876f 100644 --- a/tests/unit/python/fledge/services/core/api/control_service/test_entrypoint.py +++ b/tests/unit/python/fledge/services/core/api/control_service/test_entrypoint.py @@ -478,7 +478,19 @@ async def test__get_entrypoint(self): 'destination': 'service', 'service': 'Camera', 'constants': {'unit': 'cm'}, 'variables': {'aperture': 'f/11'}}, {'name': 'FocusCamera', 'description': 'Perform focus on camera', 'type': 'operation', 'operation_name': 'OP', 'destination': 'script', 'script': 'S1', 'anonymous': False, - 'constants': {'unit': 'cm'}, 'variables': {'aperture': 'f/16'}} + 'constants': {'unit': 'cm'}, 'variables': {'aperture': 'f/16'}}, + {'name': 'EP1', 'description': 'Entry Point', 'type': 'write', 'destination': 'broadcast', + 'constants': {'seed': '100'}, 'anonymous': True}, + {'name': 'EP2', 'description': 'Entry Point', 'type': 'write', 'destination': 'broadcast', + 'variables': {'seed': '100'}, 'anonymous': True}, + {'name': 'EP3', 'description': 'Entry Point', 'type': 'write', 'destination': 'broadcast', + 'constants': {'seed': '100', 'param2': "foo"}, 'anonymous': False, 'allow': []}, + {'name': 'EP4', 'description': 'Entry Point', 'type': 'write', 'destination': 'broadcast', + 'variables': {'seed': '100', 'param2': "foo"}, 'anonymous': False, 'allow': []}, + {'name': 'EP #5', 'description': 'Entry Point', 'type': 'write', 'destination': 'asset', "asset": "Random", + 'variables': {'seed': '100', 'param2': "foo"}, 'anonymous': True}, + {'name': 'EP-123', 'description': 'Entry Point', 'type': 'write', 'destination': 'service', "service": "S1", + 'variables': {'seed': '100', 'param2': "foo"}, 'anonymous': False, 'allow': []} ]) async def test__check_parameters(self, payload): cols = await entrypoint._check_parameters(payload) @@ -527,13 +539,16 @@ async def test__check_parameters_without_required_keys(self, payload): "Control entrypoint destination argument cannot be empty."), ({"anonymous": "t"}, ValueError, "anonymous should be a bool."), ({"constants": "t"}, ValueError, "constants should be a dictionary."), - ({"type": "write", "constants": {}}, ValueError, "constants should not be empty."), - ({"type": "write", "constants": None}, ValueError, - "For type write constants must have passed in payload and cannot have empty value."), ({"variables": "t"}, ValueError, "variables should be a dictionary."), - ({"type": "write", "constants": {"unit": "cm"}, "variables": {}}, ValueError, "variables should not be empty."), - ({"type": "write", "constants": {"unit": "cm"}, "variables": None}, ValueError, - "For type write variables must have passed in payload and cannot have empty value."), + ({"type": "write"}, ValueError, "For write type either variables or constants should not be empty."), + ({"type": "write", "constants": {}}, ValueError, + "For write type either variables or constants should not be empty."), + ({"type": "write", "variables": {}}, ValueError, + "For write type either variables or constants should not be empty."), + ({"type": "write", "constants": None}, ValueError, + "For write type either variables or constants should not be empty."), + ({"type": "write", "variables": None}, ValueError, + "For write type either variables or constants should not be empty."), ({"allow": "user"}, ValueError, "allow should be an array of list of users.") ]) async def test_bad__check_parameters(self, payload, exception_name, error_msg): From 7329b5a8d072d88f018145a3027bc92d85cecc9b Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Wed, 20 Dec 2023 13:55:28 +0000 Subject: [PATCH 44/54] FOGL-8183 Expose protocol in service record (#1250) Signed-off-by: Mark Riddoch --- C/common/include/service_record.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/C/common/include/service_record.h b/C/common/include/service_record.h index 3e36e1d8a8..d823f2bfc2 100644 --- a/C/common/include/service_record.h +++ b/C/common/include/service_record.h @@ -45,6 +45,10 @@ class ServiceRecord : public JSONProvider { { m_protocol = protocol; } + const std::string& getProtocol() const + { + return m_protocol; + } void setManagementPort(const unsigned short managementPort) { m_managementPort = managementPort; From 1118cb09c578671cb08ca003c207579f3b61f8c0 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Thu, 21 Dec 2023 15:07:51 +0530 Subject: [PATCH 45/54] properties optional attribute converted to JSON object Signed-off-by: ashish-jabble --- python/fledge/common/configuration_manager.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/python/fledge/common/configuration_manager.py b/python/fledge/common/configuration_manager.py index 9722619f2c..485e0fad92 100644 --- a/python/fledge/common/configuration_manager.py +++ b/python/fledge/common/configuration_manager.py @@ -268,7 +268,7 @@ async def _validate_category_val(self, category_name, category_val, set_value_va optional_item_entries = {'readonly': 0, 'order': 0, 'length': 0, 'maximum': 0, 'minimum': 0, 'deprecated': 0, 'displayName': 0, 'rule': 0, 'validity': 0, 'mandatory': 0, - 'group': 0, 'properties': 0} + 'group': 0} expected_item_entries = {'description': 0, 'default': 0, 'type': 0} if require_entry_value: @@ -308,10 +308,24 @@ def get_entry_val(k): raise TypeError('For {} category, entry value must be a string for item name {} and ' 'entry name {}; got {}'.format(category_name, item_name, entry_name, type(entry_val))) - elif 'type' in item_val and entry_val == 'bucket': + # Validate bucket type and mandatory properties item_name + elif 'type' in item_val and get_entry_val("type") == 'bucket': if 'properties' not in item_val: raise KeyError('For {} category, properties KV pair must be required ' 'for item name {}.'.format(category_name, item_name)) + if entry_name == 'properties': + prop_val = get_entry_val('properties') + if not isinstance(prop_val, dict): + raise ValueError('For {} category, properties must be JSON object for item name {}; got {}' + .format(category_name, item_name, type(entry_val))) + if not prop_val: + raise ValueError('For {} category, properties JSON object cannot be empty for item name {}' + ''.format(category_name, item_name)) + if 'key' not in prop_val: + raise ValueError('For {} category, key KV pair must exist in properties for item name {}' + ''.format(category_name, item_name)) + d = {entry_name: entry_val} + expected_item_entries.update(d) else: if type(entry_val) is not str: raise TypeError('For {} category, entry value must be a string for item name {} and ' @@ -335,7 +349,7 @@ def get_entry_val(k): entry_val)) is False: raise ValueError('For {} category, entry value must be an integer or float for item name ' '{}; got {}'.format(category_name, entry_name, type(entry_val))) - elif entry_name in ('displayName', 'group', 'rule', 'validity', 'properties'): + elif entry_name in ('displayName', 'group', 'rule', 'validity'): if not isinstance(entry_val, str): raise ValueError('For {} category, entry value must be string for item name {}; got {}' .format(category_name, entry_name, type(entry_val))) From ad9cd4ca8357661fd16b5781d45e35d1e16ef838 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Thu, 21 Dec 2023 15:22:33 +0530 Subject: [PATCH 46/54] configuration manager unit tests updated as per properties optional attribute in JSON object for bucket type Signed-off-by: ashish-jabble --- .../common/test_configuration_manager.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index c28db2c432..154597305a 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -547,21 +547,33 @@ async def test__validate_category_val_bucket_type_good(self, config): set_value_val_from_default_val=True) assert isinstance(c_return_value, dict) - @pytest.mark.parametrize("config", [ - ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}), - ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "property": '{"a": 1}'}}), + @pytest.mark.parametrize("config, exc_name, reason", [ + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, KeyError, + "'For {} category, properties KV pair must be required for item name {}.'".format(CAT_NAME, ITEM_NAME)), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "property": '{"a": 1}'}}, + KeyError, "'For {} category, properties KV pair must be required for item name {}.'".format( + CAT_NAME, ITEM_NAME)), ({"item": {"description": "test description", "type": "string", "default": "A", "value": "B"}, - ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}) + ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A"}}, KeyError, + "'For {} category, properties KV pair must be required for item name {}.'".format(CAT_NAME, ITEM_NAME)), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "properties": '{"a": 1}'}}, + ValueError, "For {} category, properties must be JSON object for item name {}; got ".format( + CAT_NAME, ITEM_NAME)), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "properties": {}}}, + ValueError, "For {} category, properties JSON object cannot be empty for item name {}".format( + CAT_NAME, ITEM_NAME)), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "properties": {"k": "v"}}}, + ValueError, "For {} category, key KV pair must exist in properties for item name {}".format( + CAT_NAME, ITEM_NAME)), ]) - async def test__validate_category_val_bucket_type_bad(self, config): + async def test__validate_category_val_bucket_type_bad(self, config, exc_name, reason): storage_client_mock = MagicMock(spec=StorageClientAsync) c_mgr = ConfigurationManager(storage_client_mock) - msg = "'For test category, properties KV pair must be required for item name {}.'".format(ITEM_NAME) with pytest.raises(Exception) as excinfo: await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=config, set_value_val_from_default_val=False) - assert excinfo.type is KeyError - assert msg == str(excinfo.value) + assert excinfo.type is exc_name + assert reason == str(excinfo.value) @pytest.mark.parametrize("_type, value, from_default_val", [ ("integer", " ", False), @@ -585,7 +597,7 @@ async def test__validate_category_val_with_optional_mandatory(self, _type, value test_config = {ITEM_NAME: {"description": "test description", "type": _type, "default": value, "mandatory": "true"}} if _type == "bucket": - test_config[ITEM_NAME]['properties'] = '{"foo": "bar"}' + test_config[ITEM_NAME]['properties'] = {"key": "foo"} with pytest.raises(Exception) as excinfo: await c_mgr._validate_category_val(category_name=CAT_NAME, category_val=test_config, set_value_val_from_default_val=from_default_val) @@ -3514,7 +3526,7 @@ async def async_mock(return_value): "For catname category, entry value must be string for optional item group; got "), (None, 'group', True, "For catname category, entry value must be string for optional item group; got "), - (None, 'properties', '{"foo": "bar"}', 'For catname category, optional item name properties cannot be updated.') + (None, 'properties', {"key": "Bot"}, 'For catname category, optional item name properties cannot be updated.') ]) async def test_set_optional_value_entry_bad_update(self, reset_singleton, _type, optional_key_name, new_value_entry, exc_msg): @@ -3536,7 +3548,7 @@ async def async_mock(return_value): 'deprecated': 'false', 'readonly': 'true', 'type': 'string', 'order': '4', 'description': 'Test Optional', 'minimum': minimum, 'value': '13', 'maximum': maximum, 'default': '13', 'validity': 'field X is set', 'mandatory': 'false', 'group': 'Security', - 'properties': "{}"} + 'properties': {"key": "model"}} # Changed in version 3.8: patch() now returns an AsyncMock if the target is an async function. if sys.version_info.major == 3 and sys.version_info.minor >= 8: @@ -3548,6 +3560,7 @@ async def async_mock(return_value): with patch.object(ConfigurationManager, '_read_item_val', return_value=_rv) as readpatch: with pytest.raises(Exception) as excinfo: await c_mgr.set_optional_value_entry(category_name, item_name, optional_key_name, new_value_entry) + assert excinfo.type is ValueError assert exc_msg == str(excinfo.value) readpatch.assert_called_once_with(category_name, item_name) From 81983564cc681f780c7e0e11ae7f994f390da7f7 Mon Sep 17 00:00:00 2001 From: ashish-jabble Date: Thu, 21 Dec 2023 17:18:07 +0530 Subject: [PATCH 47/54] default value error handling for bucket type Signed-off-by: ashish-jabble --- python/fledge/common/configuration_manager.py | 5 +++++ .../unit/python/fledge/common/test_configuration_manager.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/python/fledge/common/configuration_manager.py b/python/fledge/common/configuration_manager.py index 485e0fad92..57d1885bca 100644 --- a/python/fledge/common/configuration_manager.py +++ b/python/fledge/common/configuration_manager.py @@ -326,6 +326,11 @@ def get_entry_val(k): ''.format(category_name, item_name)) d = {entry_name: entry_val} expected_item_entries.update(d) + else: + if type(entry_val) is not str: + raise TypeError('For {} category, entry value must be a string for item name {} and ' + 'entry name {}; got {}'.format(category_name, item_name, entry_name, + type(entry_val))) else: if type(entry_val) is not str: raise TypeError('For {} category, entry value must be a string for item name {} and ' diff --git a/tests/unit/python/fledge/common/test_configuration_manager.py b/tests/unit/python/fledge/common/test_configuration_manager.py index 154597305a..3587e48c00 100644 --- a/tests/unit/python/fledge/common/test_configuration_manager.py +++ b/tests/unit/python/fledge/common/test_configuration_manager.py @@ -565,6 +565,9 @@ async def test__validate_category_val_bucket_type_good(self, config): ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": "A", "properties": {"k": "v"}}}, ValueError, "For {} category, key KV pair must exist in properties for item name {}".format( CAT_NAME, ITEM_NAME)), + ({ITEM_NAME: {"description": "test description", "type": "bucket", "default": {}, "properties": {"key": "v"}}}, + TypeError, "For {} category, entry value must be a string for item name {} and entry name default; " + "got ".format(CAT_NAME, ITEM_NAME)) ]) async def test__validate_category_val_bucket_type_bad(self, config, exc_name, reason): storage_client_mock = MagicMock(spec=StorageClientAsync) From fed950975101bd786bdd881374cbaffe56e5e30f Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Thu, 21 Dec 2023 14:07:27 +0000 Subject: [PATCH 48/54] FOGL-8353 Resolve issue if first row in a columns contains the string (#1251) * FOGL-8353 Resolve issue if firs t row in a columns contains the string "true" or "false". Signed-off-by: Mark Riddoch * Check for boolean in string columns Signed-off-by: Mark Riddoch --------- Signed-off-by: Mark Riddoch --- C/common/result_set.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/C/common/result_set.cpp b/C/common/result_set.cpp index 953d15dd68..c79482d969 100644 --- a/C/common/result_set.cpp +++ b/C/common/result_set.cpp @@ -92,7 +92,14 @@ ResultSet::ResultSet(const std::string& json) switch (m_columns[colNo]->getType()) { case STRING_COLUMN: - rowValue->append(new ColumnValue(string(item->value.GetString()))); + if (item->value.IsBool()) + { + rowValue->append(new ColumnValue(item->value.IsTrue() ? "true" : "false")); + } + else + { + rowValue->append(new ColumnValue(string(item->value.GetString()))); + } break; case INT_COLUMN: rowValue->append(new ColumnValue((long)(item->value.GetInt64()))); @@ -104,8 +111,10 @@ ResultSet::ResultSet(const std::string& json) rowValue->append(new ColumnValue(item->value)); break; case BOOL_COLUMN: - // TODO Add support - rowValue->append(new ColumnValue(string("TODO"))); + if (item->value.IsString()) + rowValue->append(new ColumnValue(string(item->value.GetString()))); + else + rowValue->append(new ColumnValue(item->value.IsTrue() ? "true" : "false")); break; } colNo++; From 864875e27714a9165170c01a366901932f86c4ff Mon Sep 17 00:00:00 2001 From: FlorentP42 <45787476+FlorentP42@users.noreply.github.com> Date: Fri, 22 Dec 2023 11:18:48 +0100 Subject: [PATCH 49/54] refs #1234 Made readings variable reinitialized each loop. Cleared readings variable in case of exception while filling it. (#1235) Signed-off-by: Florent Peyrusse Co-authored-by: Florent Peyrusse Co-authored-by: Mark Riddoch --- C/services/north/data_load.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/C/services/north/data_load.cpp b/C/services/north/data_load.cpp index da15df6c0e..8d721395ef 100755 --- a/C/services/north/data_load.cpp +++ b/C/services/north/data_load.cpp @@ -156,11 +156,10 @@ void DataLoad::triggerRead(unsigned int blockSize) */ void DataLoad::readBlock(unsigned int blockSize) { -ReadingSet *readings = NULL; -int n_waits = 0; - + int n_waits = 0; do { + ReadingSet* readings = nullptr; try { switch (m_dataSource) @@ -178,16 +177,19 @@ int n_waits = 0; default: Logger::getLogger()->fatal("Bad source for data to send"); break; - } } - catch (ReadingSetException* e) + catch (ReadingSetException* e) { // Ignore, the exception has been reported in the layer below + // readings may contain erroneous data, clear it + readings = nullptr; } - catch (exception& e) + catch (exception& e) { // Ignore, the exception has been reported in the layer below + // readings may contain erroneous data, clear it + readings = nullptr; } if (readings && readings->getCount()) { @@ -211,7 +213,7 @@ int n_waits = 0; // Logger::getLogger()->debug("DataLoad::readBlock(): No readings available"); } if (!m_shutdown) - { + { // TODO improve this this_thread::sleep_for(chrono::milliseconds(250)); n_waits++; From 1f14ed732d0fad52d2638ab3354bc0b26e5dcc26 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Arora Date: Fri, 22 Dec 2023 16:26:16 +0530 Subject: [PATCH 50/54] ConfigCategory support for Bucket properties in JSON format rather than string Signed-off-by: Amandeep Singh Arora --- C/common/config_category.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/C/common/config_category.cpp b/C/common/config_category.cpp index ffe63e0353..1dbeb64b1b 100755 --- a/C/common/config_category.cpp +++ b/C/common/config_category.cpp @@ -1094,7 +1094,20 @@ ConfigCategory::CategoryItem::CategoryItem(const string& name, if (item.HasMember("properties")) { - m_bucketProperties = item["properties"].GetString(); + Logger::getLogger()->debug("item['properties'].IsString()=%s, item['properties'].IsObject()=%s", + item["properties"].IsString()?"true":"false", + item["properties"].IsObject()?"true":"false"); + + rapidjson::StringBuffer strbuf; + rapidjson::Writer writer(strbuf); + item["properties"].Accept(writer); + m_bucketProperties = item["properties"].IsObject() ? + // use current string + strbuf.GetString() : + // Unescape the string + JSONunescape(strbuf.GetString()); + + Logger::getLogger()->debug("m_bucketProperties=%s", m_bucketProperties.c_str()); } else { @@ -1493,7 +1506,7 @@ ostringstream convert; if (!m_bucketProperties.empty()) { - convert << ", \"properties\" : \"" << JSONescape(m_bucketProperties) << "\""; + convert << ", \"properties\" : " << m_bucketProperties; } if (!m_group.empty()) @@ -1569,7 +1582,7 @@ ostringstream convert; if (!m_bucketProperties.empty()) { - convert << ", \"properties\" : \"" << JSONescape(m_bucketProperties) << "\""; + convert << ", \"properties\" : " << m_bucketProperties; } if (!m_group.empty()) From 5f29f01fa5dcba323d3c5cdcd18ec994babcfca1 Mon Sep 17 00:00:00 2001 From: Mark Riddoch Date: Fri, 22 Dec 2023 14:07:41 +0000 Subject: [PATCH 51/54] FOGL-8365 Removed reserved characters from container names (#1254) Signed-off-by: Mark Riddoch --- C/plugins/north/OMF/linkdata.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/C/plugins/north/OMF/linkdata.cpp b/C/plugins/north/OMF/linkdata.cpp index af5f6fe9ad..91e4612f28 100644 --- a/C/plugins/north/OMF/linkdata.cpp +++ b/C/plugins/north/OMF/linkdata.cpp @@ -434,7 +434,8 @@ void OMFLinkedData::sendContainer(string& linkName, Datapoint *dp, OMFHints * hi container += "\", \"typeid\" : \""; container += baseType; container += "\", \"name\" : \""; - container += dp->getName(); + string dpName = OMF::ApplyPIServerNamingRulesObj(dp->getName(), NULL); + container += dpName; container += "\", \"datasource\" : \"" + dataSource + "\""; if (propertyOverrides) From 126520eafe63d645673f56e8d17e05a06c5be16e Mon Sep 17 00:00:00 2001 From: dianomicbot Date: Thu, 28 Dec 2023 07:57:03 +0000 Subject: [PATCH 52/54] VERSION changed Signed-off-by: dianomicbot --- VERSION | 2 +- docs/91_version_history.rst | 35 +++++++++++++++++++++++++++++++++++ docs/conf.py | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index ed70b8404e..ed5d342137 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -fledge_version=2.2.0 +fledge_version=2.3.0 fledge_schema=66 diff --git a/docs/91_version_history.rst b/docs/91_version_history.rst index 7c409f4108..8077659c84 100644 --- a/docs/91_version_history.rst +++ b/docs/91_version_history.rst @@ -25,6 +25,41 @@ Version History Fledge v2 ========== +v2.3.0 +------- + +Release Date: 2023-12-28 + +- **Fledge Core** + + - New Features: + + + + - Bug Fix: + + + +- **GUI** + + - New Features: + + + + - Bug Fix: + + + +- **Plugins** + + - New Features: + + + + - Bug Fix: + + + v2.2.0 ------- diff --git a/docs/conf.py b/docs/conf.py index 4069ee0648..de97a54cdc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,4 +177,4 @@ # Pass Plugin DOCBRANCH argument in Makefile ; by default develop # NOTE: During release time we need to replace DOCBRANCH with actual released version -subprocess.run(["make generated DOCBRANCH='develop'"], shell=True, check=True) +subprocess.run(["make generated DOCBRANCH='2.3.0RC'"], shell=True, check=True) From de9db8bfe3b20c79cdcaf39f284dfefee5da61cf Mon Sep 17 00:00:00 2001 From: Ashish Jabble Date: Fri, 5 Jan 2024 16:03:03 +0530 Subject: [PATCH 53/54] PR 1256 1257 cherry pick (#1259) * Added AVEVA version information Add the section OMF Version Support to document the version of OMF that will be used to post data to the various versions of PI Web API, Edge Data Store (EDS), and AVEVA Data Hub (ADH). Signed-off-by: Ray Verhoeff * Renamed Send full structure configuration boolean Renamed configuration boolean Send full structure to Create AF Structure. Signed-off-by: Ray Verhoeff * Updated OMF_Default Updated OMF_Default to rename "Send full structure" to "Create AF Structure." Signed-off-by: Ray Verhoeff * Merge pull request #1256 from fledge-iot/FOGL-7649 FOGL-7649: Document OMF and AVEVA product versions * FOGL-8346 Fix lookup data index when using TagName hints. (#1257) * FOGL-8365 Removed reserved characters from container names Signed-off-by: Mark Riddoch * FOGL-8365 Fix lookup data index when using TagName hints. Also fix AF placement and dynamic changing of TagName hint value such that it recreates containers in an efficient fashion. Signed-off-by: Mark Riddoch --------- Signed-off-by: Mark Riddoch --------- Signed-off-by: Ray Verhoeff Signed-off-by: Mark Riddoch Co-authored-by: Ray Verhoeff Co-authored-by: Mark Riddoch --- C/plugins/north/OMF/include/linkedlookup.h | 47 ++++++++++++++++--- C/plugins/north/OMF/linkdata.cpp | 52 ++++++++++++++++----- C/plugins/north/OMF/omf.cpp | 26 ++++++++++- docs/OMF.rst | 27 ++++++++++- docs/images/OMF_Default.jpg | Bin 83230 -> 45838 bytes 5 files changed, 132 insertions(+), 20 deletions(-) diff --git a/C/plugins/north/OMF/include/linkedlookup.h b/C/plugins/north/OMF/include/linkedlookup.h index a38cdbba2a..560b3da251 100644 --- a/C/plugins/north/OMF/include/linkedlookup.h +++ b/C/plugins/north/OMF/include/linkedlookup.h @@ -28,20 +28,53 @@ typedef enum { class LALookup { public: LALookup() { m_sentState = 0; m_baseType = OMFBT_UNKNOWN; }; - bool assetState() { return (m_sentState & LAL_ASSET_SENT) != 0; }; - bool linkState() { return (m_sentState & LAL_LINK_SENT) != 0; }; - bool containerState() { return (m_sentState & LAL_CONTAINER_SENT) != 0; }; + bool assetState(const std::string& tagName) + { + return ((m_sentState & LAL_ASSET_SENT) != 0) + && (m_tagName.compare(tagName) == 0); + }; + bool linkState(const std::string& tagName) + { + return ((m_sentState & LAL_LINK_SENT) != 0) + && (m_tagName.compare(tagName) == 0); + }; + bool containerState(const std::string& tagName) + { + return ((m_sentState & LAL_CONTAINER_SENT) != 0) + && (m_tagName.compare(tagName) == 0); + }; bool afLinkState() { return (m_sentState & LAL_AFLINK_SENT) != 0; }; void setBaseType(const std::string& baseType); OMFBaseType getBaseType() { return m_baseType; }; std::string getBaseTypeString(); - void assetSent() { m_sentState |= LAL_ASSET_SENT; }; - void linkSent() { m_sentState |= LAL_LINK_SENT; }; + void assetSent(const std::string& tagName) + { + if (m_tagName.compare(tagName)) + { + m_sentState = LAL_ASSET_SENT; + m_tagName = tagName; + } + else + { + m_sentState |= LAL_ASSET_SENT; + } + }; + void linkSent(const std::string& tagName) + { + if (m_tagName.compare(tagName)) + { + // Force the container to resend if the tagName changes + m_tagName = tagName; + m_sentState &= ~LAL_CONTAINER_SENT; + } + m_sentState |= LAL_LINK_SENT; + }; void afLinkSent() { m_sentState |= LAL_AFLINK_SENT; }; - void containerSent(const std::string& baseType); - void containerSent(OMFBaseType baseType) { m_baseType = baseType; }; + void containerSent(const std::string& tagName, const std::string& baseType); + void containerSent(const std::string& tagName, OMFBaseType baseType); private: uint8_t m_sentState; OMFBaseType m_baseType; + std::string m_tagName; }; #endif diff --git a/C/plugins/north/OMF/linkdata.cpp b/C/plugins/north/OMF/linkdata.cpp index 91e4612f28..5e1a83cc85 100644 --- a/C/plugins/north/OMF/linkdata.cpp +++ b/C/plugins/north/OMF/linkdata.cpp @@ -76,6 +76,8 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi string assetName = reading.getAssetName(); + string originalAssetName = OMF::ApplyPIServerNamingRulesObj(assetName, NULL); + // Apply any TagName hints to modify the containerid if (hints) { @@ -109,14 +111,14 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi assetName = OMF::ApplyPIServerNamingRulesObj(assetName, NULL); bool needDelim = false; - auto assetLookup = m_linkedAssetState->find(assetName + "."); + auto assetLookup = m_linkedAssetState->find(originalAssetName + "."); if (assetLookup == m_linkedAssetState->end()) { // Panic Asset lookup not created - Logger::getLogger()->fatal("FIXME: no asset lookup item for %s.", assetName.c_str()); + Logger::getLogger()->error("Internal error: No asset lookup item for %s.", assetName.c_str()); return ""; } - if (m_sendFullStructure && assetLookup->second.assetState() == false) + if (m_sendFullStructure && assetLookup->second.assetState(assetName) == false) { // Send the data message to create the asset instance outData.append("{ \"typeid\":\"FledgeAsset\", \"values\":[ { \"AssetId\":\""); @@ -124,7 +126,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi outData.append(assetName + "\""); outData.append("} ] }"); needDelim = true; - assetLookup->second.assetSent(); + assetLookup->second.assetSent(assetName); } /** @@ -183,17 +185,18 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi // Create the link for the asset if not already created string link = assetName + "." + dpName; - auto dpLookup = m_linkedAssetState->find(link); + string dpLookupName = originalAssetName + "." + dpName; + auto dpLookup = m_linkedAssetState->find(dpLookupName); string baseType = getBaseType(dp, format); if (dpLookup == m_linkedAssetState->end()) { Logger::getLogger()->error("Trying to send a link for a datapoint for which we have not created a base type"); } - else if (dpLookup->second.containerState() == false) + else if (dpLookup->second.containerState(assetName) == false) { sendContainer(link, dp, hints, baseType); - dpLookup->second.containerSent(baseType); + dpLookup->second.containerSent(assetName, baseType); } else if (baseType.compare(dpLookup->second.getBaseTypeString()) != 0) { @@ -210,7 +213,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi else { sendContainer(link, dp, hints, baseType); - dpLookup->second.containerSent(baseType); + dpLookup->second.containerSent(assetName, baseType); } } if (baseType.empty()) @@ -219,7 +222,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi skippedDatapoints.push_back(dpName); continue; } - if (m_sendFullStructure && dpLookup->second.linkState() == false) + if (m_sendFullStructure && dpLookup->second.linkState(assetName) == false) { outData.append("{ \"typeid\":\"__Link\","); outData.append("\"values\":[ { \"source\" : {"); @@ -229,7 +232,7 @@ string OMFLinkedData::processReading(const Reading& reading, const string& AFHi outData.append("\"containerid\" : \""); outData.append(link); outData.append("\" } } ] },"); - dpLookup->second.linkSent(); + dpLookup->second.linkSent(assetName); } // Convert reading data into the OMF JSON string @@ -595,10 +598,37 @@ void LALookup::setBaseType(const string& baseType) /** * The container has been sent with the specific base type + * + * @param tagName The name of the tag we are using + * @param baseType The baseType we resolve to + */ +void LALookup::containerSent(const std::string& tagName, OMFBaseType baseType) +{ + if (m_tagName.compare(tagName)) + { + // Force a new Link and AF Link to be sent for the new tag name + m_sentState &= ~(LAL_LINK_SENT | LAL_AFLINK_SENT); + } + m_baseType = baseType; + m_tagName = tagName; + m_sentState |= LAL_CONTAINER_SENT; +} + +/** + * The container has been sent with the specific base type + * + * @param tagName The name of the tag we are using + * @param baseType The baseType we resolve to */ -void LALookup::containerSent(const std::string& baseType) +void LALookup::containerSent(const std::string& tagName, const std::string& baseType) { setBaseType(baseType); + if (m_tagName.compare(tagName)) + { + // Force a new Link and AF Link to be sent for the new tag name + m_sentState &= ~(LAL_LINK_SENT | LAL_AFLINK_SENT); + } + m_tagName = tagName; m_sentState |= LAL_CONTAINER_SENT; } diff --git a/C/plugins/north/OMF/omf.cpp b/C/plugins/north/OMF/omf.cpp index 53c1e7c4a9..f7ddc040be 100755 --- a/C/plugins/north/OMF/omf.cpp +++ b/C/plugins/north/OMF/omf.cpp @@ -2302,6 +2302,7 @@ std::string OMF::createLinkData(const Reading& reading, std::string& AFHierarch long typeId = getAssetTypeId(assetName); + string lData = "{\"typeid\": \"__Link\", \"values\": ["; // Handles the structure for the Connector Relay @@ -2354,8 +2355,31 @@ std::string OMF::createLinkData(const Reading& reading, std::string& AFHierarch } else { + // Get the new asset name after hints are applied for the linked data messages + string newAssetName = assetName; + if (hints) + { + const std::vector omfHints = hints->getHints(); + for (auto it = omfHints.cbegin(); it != omfHints.cend(); it++) + { + if (typeid(**it) == typeid(OMFTagNameHint)) + { + string hintValue = (*it)->getHint(); + Logger::getLogger()->info("Using OMF TagName hint: %s for asset %s", + hintValue.c_str(), assetName.c_str()); + newAssetName = hintValue; + } + if (typeid(**it) == typeid(OMFTagHint)) + { + string hintValue = (*it)->getHint(); + Logger::getLogger()->info("Using OMF Tag hint: %s for asset %s", + hintValue.c_str(), assetName.c_str()); + newAssetName = hintValue; + } + } + } StringReplace(tmpStr, "_placeholder_tgt_type_", "FledgeAsset"); - StringReplace(tmpStr, "_placeholder_tgt_idx_", assetName); + StringReplace(tmpStr, "_placeholder_tgt_idx_", newAssetName); } lData.append(tmpStr); diff --git a/docs/OMF.rst b/docs/OMF.rst index fdbda34529..9ee26c68a9 100644 --- a/docs/OMF.rst +++ b/docs/OMF.rst @@ -132,7 +132,7 @@ The *Default Configuration* tab contains the most commonly modified items - *Edge Data Store* - The OSISoft Edge Data Store - - **Send full structure**: Used to control if Asset Framework structure messages are sent to the PI Server. If this is turned off then the data will not be placed in the Asset Framework. + - **Create AF Structure**: Used to control if Asset Framework structure messages are sent to the PI Server. If this is turned off then the data will not be placed in the Asset Framework. - **Naming scheme**: Defines the naming scheme to be used when creating the PI points in the PI Data Archive. See :ref:`Naming_Scheme`. @@ -667,3 +667,28 @@ Versions of this plugin prior to 2.1.0 created a complex type within OMF for eac As of version 2.1.0 this linking approach is used for all new assets created, if assets exist within the PI Server from versions of the plugin prior to 2.1.0 then the older, complex types will be used. It is possible to force the plugin to use complex types for all assets, both old and new, using the configuration option. It is also to force a particular asset to use the complex type mechanism using an OMFHint. +OMF Version Support +------------------- + +To date, AVEVA has released three versions of the OSIsoft Message Format (OMF) specification: 1.0, 1.1 and 1.2. +The OMF Plugin supports all three OMF versions. +The plugin will determine the OMF version to use by reading product version information from the AVEVA data destination system. +These are the OMF versions the plugin will use to post data: + ++-----------+----------+---------------------+ +|OMF Version|PI Web API|Edge Data Store (EDS)| ++===========+==========+=====================+ +| 1.2|- 2021 |- 2023 | +| |- 2021 SP1|- 2023 Patch 1 | +| |- 2021 SP2| | +| |- 2021 SP3| | +| |- 2023 | | ++-----------+----------+---------------------+ +| 1.1|- 2019 | | +| |- 2019 SP1| | ++-----------+----------+---------------------+ +| 1.0| |- 2020 | ++-----------+----------+---------------------+ + +The AVEVA Data Hub (ADH) is cloud-deployed and is always at the latest version of OMF support which is 1.2. +This includes the legacy OSIsoft Cloud Services (OCS) endpoints. diff --git a/docs/images/OMF_Default.jpg b/docs/images/OMF_Default.jpg index 3d7b17ec09697a330ca91815e24f8e77d8183d10..3f6df81b2eb6e5fa5ecfcd763be00ce7d12772a2 100644 GIT binary patch literal 45838 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 From f73b9f35993bacc6b1d45de32967c7778405ea56 Mon Sep 17 00:00:00 2001 From: Mohit04tomar <43023917+Mohit04tomar@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:01:44 +0530 Subject: [PATCH 54/54] Updated 91_version_history file for 2.3.0 release (#1260) --- docs/91_version_history.rst | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/91_version_history.rst b/docs/91_version_history.rst index 8077659c84..327879bcd0 100644 --- a/docs/91_version_history.rst +++ b/docs/91_version_history.rst @@ -34,30 +34,62 @@ Release Date: 2023-12-28 - New Features: - + - A new REST API has been added to the public API to allow performance counters to be fetched and reset. This API is intended for diagnostic purposes only. + - Improvements have been made to the way load issues on the storage service are logged. + - Documentation has been added that describes how to extend the API to include custom URLs for executing control functions. This documentation also shows how these are then called using the graphical user interface. - Bug Fix: + - An issue with the PostgreSQL storage plugin when very large numbers of readings are ingested, more than 4294967296, has now been resolved. + - An issue with services shutting down rather than restarting when they fail to get a valid bearer token has been resolved. + - The user interface for creating write API endpoints was incorrectly requiring both a constant and a variable when only one is required. This is now resolved. + - A problem that meant parameters to set point control operations were not correctly sent to south plugins written in Python has been resolved. - **GUI** - New Features: + - The user interface has been upgraded to use Angular version 16. + - The configuration section of the user interface that allows for instance wide configuration has been improved with a single tree navigation item and improved visual feedback. + - A link to the documentation has been added to the Control API pages of the user interface. - Bug Fix: + - An issue that could cause some datapoint to display incorrectly in the user interface graph when multiple assets are displayed and those assets have data points with the same name in both assets has been resolved. + - An issue in the user interface that meant exporting data as a CSV file created incorrect files if any of the data point names contained a comma has been fixed. + - An issue with the user interface not always correctly showing the information for the dispatcher service has been resolved. + - A broken link to the documentation in the control pipeline user interface page of the user interface has been fixed. -- **Plugins** +- **Services & Plugins** - New Features: + - The benchmark south plugin has been enhanced to increase the load that can be placed during testing. + - The fldege-south-s2opcua south plugin has been enhanced to allow filtering of nodes using regular expressions on the Browse Name of the nodes. + - The OMF north plugin has been updated to improve both the time and space efficiency of the lookup data used to map to PI Server objects. + - OMF North plugin documentation has been updated to show which version of the OMF specification the plugin will adopt when communicating with different versions of AVEVA products: PI Web API, Edge Data Store (EDS) and AVEVA Data Hub (ADH). - Bug Fix: + - A memory leak in the SQLite in-memory storage plugin has been resolved. + - A memory leak in the OMF north plugin has been resolved. + - An issue that could cause data to fail to send using the OMF plugin when the names of data points contain special characters has now been resolved. + - When the "Send full structure" configuration boolean was false, OMF North would create an AF structure anyways. All AF Elements were at the root of the AF database, with every AF Element having a single AF Attribute mapped to a PI Point. Creation of this AF structure would take a long time for large databases which would lead to PI Web API POST timeouts. This has been fixed. If the configuration boolean is false, OMF North will create PI Points only. In the configuration page, Send full structure has been renamed to "Create AF Structure". + - The OMF North plugin was unable to connect to AVEVA Data Hub (ADH) and OSIsoft Cloud Services (OCS) endpoints. This has been fixed. + - An issue with using an OMF Hint that defines a specific name to use with a tag has been resolved. The issue would show itself as the data not being sent to PI or ADH in some circumstances. + - An issue that meant some OPC UA nodes stored in the root of the hierarchy were not correctly ingested in the fldege-south-s2opcua south plugin has been resolved. + - The SQLite storage plugin had an issue that caused it to create overflow tables multiple times. This was not a problem in itself, but did cause the database to become locked for excessive periods of time, creating contention and delays for data ingestions in progress at the time. + - A problem that, in rare circumstances, could result in data being added to the incorrect asset in the SQLite plugin has been resolved. + - An issue with assets containing bracket characters not being stored in the PostgreSQL storage plugin has been resolved. + - An issue with string type parameters to control operations having extra pairs of quotes added has been resolved. + - A problem that caused the dispatcher service to log messages regarding incorrect bearer tokens has been resolved. + - The control dispatcher service was previously advertising itself before it had completed initialisation. This meant that a request could be received when it was partially configured, resulting in a crash of the service. Registration now takes place only once the service is completely ready to accept requests. + - The control dispatcher was not always using the correct source information when looking for matching pipelines. This has now been resolved. + - Control pipelines were previously still being executed if the entire pipeline was disabled, this has now been resolved. v2.2.0