diff --git a/README.md b/README.md index 1dbcdc1d..6ba4b4f8 100755 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ The Application Developers Guide is the best document to read first. - A template for the creation of a new application utilizing the csclient library. - **app_holder** - Just a holder for dynamic_app. See dynamic_app. +- **appdata_sample** + - Store and retrieve settings in SDK Appdata in configuration. - **Autoinstall** - Automatically choose fastest SIM on install. On bootup, AutoInstall detects SIMs, and ensures (clones) they have unique WAN profiles for prioritization. Then the app collects diagnostics and runs Ookla speedtests on each SIM. Then the app prioritizes the SIMs WAN Profiles by TCP download speed. Results are written to the log, set as the description field, and sent as a custom alert. The app can be manually triggered again by clearing out the description field in NCM. - **Installer_UI** diff --git a/appdata_sample/appdata_sample.py b/appdata_sample/appdata_sample.py new file mode 100644 index 00000000..7a930b7a --- /dev/null +++ b/appdata_sample/appdata_sample.py @@ -0,0 +1,32 @@ +# appdata_sample - example of how to use SDK Appdata to store and retrieve settings in NCOS Configs. +# the get_appdata() function will return the value of the appdata entry with the specified name. +# if the appdata is not found, it will save the default_appdata to the NCOS Configs and return it. + +from csclient import EventingCSClient +import json +import time + +default_appdata = { + "server": "192.168.0.1", + "port": 8000 +} + +def get_appdata(name): + """Get appdata from NCOS Configs. If not found, save default_appdata and return it.""" + try: + appdata = cp.get('config/system/sdk/appdata') + data = json.loads([x["value"] for x in appdata if x["name"] == name][0]) + except: + data = default_appdata + cp.post('config/system/sdk/appdata', {"name": name, "value": json.dumps(data)}) + cp.log(f'No appdata found - Saved default: {data}') + return data + +cp = EventingCSClient('appdata_sample') +cp.log('Starting...') + +# Run a loop to get the appdata every 10 seconds so you can see user changes. +while True: + appdata = get_appdata('appdata_sample') + cp.log(f'Appdata: {appdata}') + time.sleep(10) diff --git a/appdata_sample/csclient.py b/appdata_sample/csclient.py new file mode 100644 index 00000000..5423c804 --- /dev/null +++ b/appdata_sample/csclient.py @@ -0,0 +1,608 @@ +""" +NCOS communication module for SDK applications. + +Copyright (c) 2022 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. +""" + + +import json +import os +import re +import select +import socket +import threading +import logging.handlers +import signal +import sys + +try: + import traceback +except ImportError: + traceback = None + + +class SdkCSException(Exception): + pass + + +class CSClient(object): + """ + The CSClient class is the NCOS SDK mechanism for communication between apps and the router tree/config store. + Instances of this class communicate with the router using either an explicit socket or with http method calls. + + Apps running locally on the router use a socket on the router to send commands from the app to the router tree + and to receive data (JSON) from the router tree. + + Apps running remotely use the requests library to send HTTP method calls to the router and to receive data from + the router tree. This allows one to use an IDE to run and debug the application on a the computer. Although, + there are limitations with respect to the device hardware access (i.e. serial, USB, etc.). + """ + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return cls in cls._instances + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, app_name, init=False): + self.app_name = app_name + self.ncos = '/var/mnt/sdk' in os.getcwd() # Running in NCOS + handlers = [logging.StreamHandler()] + if 'linux' in sys.platform: + handlers.append(logging.handlers.SysLogHandler(address='/dev/log')) + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s: %(message)s', datefmt='%b %d %H:%M:%S', + handlers=handlers) + self.logger = logging.getLogger(app_name) + if not init: + return + + def get(self, base, query='', tree=0): + """ + Constructs and sends a get request to retrieve specified data from a device. + + The behavior of this method is contextual: + - If the app is installed on (and executed from) a device, it directly queries the router tree to retrieve the + specified data. + - If the app running remotely from a computer it calls the HTTP GET method to retrieve the specified data. + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + + """ + if 'linux' in sys.platform: + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd).get('data') + else: + # Running in a computer so use http to send the get to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.get(device_api, auth=self._get_auth(device_ip, username, password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text).get('data') + + def decrypt(self, base, query='', tree=0): + """ + Constructs and sends a decrypt/get request to retrieve specified data from a device. + + The behavior of this method is contextual: + - If the app is installed on (and executed from) a device, it directly queries the router tree to retrieve the + specified data. + - If the app running remotely from a computer it calls the HTTP GET method to retrieve the specified data. + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + + """ + if 'linux' in sys.platform: + cmd = "decrypt\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd).get('data') + else: + # Running in a computer and can't actually send the alert. + print('Decrypt is only available when running the app in NCOS.') + + def put(self, base, value='', query='', tree=0): + """ + Constructs and sends a put request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP PUT method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + value = json.dumps(value) + if 'linux' in sys.platform: + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.put(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def post(self, base, value='', query=''): + """ + Constructs and sends a post request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP POST method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + value = json.dumps(value) + if 'linux' in sys.platform: + cmd = f"post\n{base}\n{query}\n{value}\n" + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.post(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def patch(self, value): + """ + Constructs and sends a patch request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP PUT method to update or add the specified + data. + + Args: + value: list containing dict of add/changes, and list of removals: [{add},[remove]] + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + + if 'linux' in sys.platform: + if value[0].get("config"): + adds = value[0] + else: + adds = {"config": value[0]} + adds = json.dumps(adds) + removals = json.dumps(value[1]) + cmd = f"patch\n{adds}\n{removals}\n" + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/'.format(device_ip) + + try: + response = requests.patch(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(json.dumps(value))}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def delete(self, base, query=''): + """ + Constructs and sends a delete request to delete specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly deletes the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP DELETE method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + query: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + if 'linux' in sys.platform: + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.delete(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(base)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def alert(self, value=''): + """ + Constructs and sends a custom alert to NCM for the device. Apps calling this method must be running + on the target device to send the alert. If invoked while running on a computer, then only a log is output. + + Args: + + app_name: String name of your application. + value: String to displayed for the alert. + + Returns: + Success: None + Failure: An error + """ + if 'linux' in sys.platform: + cmd = "alert\n{}\n{}\n".format(self.app_name, value) + return self._dispatch(cmd) + else: + # Running in a computer and can't actually send the alert. + print('Alert is only available when running the app in NCOS.') + print('Alert Text: {}'.format(value)) + + def log(self, value=''): + """ + Adds an INFO log to the device SYSLOG. + + Args: + value: String text for the log. + + Returns: + None + """ + if self.ncos: + # Running in NCOS so write to the logger + self.logger.info(value) + elif 'linux' in sys.platform: + # Running in Linux (container?) so write to stdout + with open('/dev/stdout', 'w') as log: + log.write(f'{self.app_name}: {value}\n') + else: + # Running in a computer so just use print for the log. + print(value) + + + def _get_auth(self, device_ip, username, password): + # This is only needed when the app is running in a computer. + # Returns the proper HTTP Auth for the global username and password. + # Digest Auth is used for NCOS 6.4 and below while Basic Auth is + # used for NCOS 6.5 and up. + import requests + from http import HTTPStatus + + use_basic = False + device_api = 'http://{}/api/status/product_info'.format(device_ip) + + try: + response = requests.get(device_api, auth=requests.auth.HTTPBasicAuth(username, password)) + if response.status_code == HTTPStatus.OK: + use_basic = True + + except: + use_basic = False + + if use_basic: + return requests.auth.HTTPBasicAuth(username, password) + else: + return requests.auth.HTTPDigestAuth(username, password) + + @staticmethod + def _get_device_access_info(): + # Should only be called when running in a computer. It will return the + # dev_client_ip, dev_client_username, and dev_client_password as defined in + # the sdk section of the sdk_settings.ini file. + device_ip = '' + device_username = '' + device_password = '' + + if 'linux' not in sys.platform: + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + device_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + device_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + device_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return device_ip, device_username, device_password + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(errmsg) + return result + + +class EventingCSClient(CSClient): + running = False + registry = {} + eids = 1 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.on = self.register + self.un = self.unregister + + def start(self): + if self.running: + self.log(f"Eventing Config Store {self.pid} already running") + return + self.running = True + self.pid = os.getpid() + self.f = '/var/tmp/csevent_%d.sock' % self.pid + try: + os.unlink(self.f) + except FileNotFoundError: + pass + self.event_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.event_sock.bind(self.f) + self.event_sock.listen() # backlog is optional. already set on value found in /proc + self.event_sock.setblocking(False) + self.eloop = threading.Thread(target=self._handle_events) + self.eloop.start() + + def stop(self): + if not self.running: + return + self.log(f"Stopping") + for k in list(self.registry.keys()): + self.unregister(k) + self.event_sock.close() + os.unlink(self.f) + self.running = False + + def _handle_events(self): + poller = select.poll() + poller.register(self.event_sock, + select.POLLIN | select.POLLERR | select.POLLHUP) # I don't unregsiter this in cleaning up! + while self.running: + try: + events = poller.poll(1000) + for f, ev in events: + if ev & (select.POLLERR | select.POLLHUP): + self.log("Hangup/error received. Stopping") + self.stop() # TODO: restart w/ cached registrations. Will no longer be an error case + + if ev & select.POLLIN: + conn, addr = self.event_sock.accept() + result = self._receive(conn) + eid = int(result['data']['id']) + try: + cb = self.registry[eid]['cb'] + args = self.registry[eid]['args'] + try: + # PUTting just a string to config store results in a json encoded string returned. + # e.g. set /config/system/logging/level "debug", result['data']['cfg'] is '"debug"' + cfg = json.loads(result['data']['cfg']) + except TypeError as e: + # Non-string path + cfg = result['data']['cfg'] + try: + cb_return = cb(result['data']['path'], cfg, args) + except: + if traceback: + traceback.print_exc() + self.log(f"Exception during callback for {str(self.registry[eid])}") + if result['data']['action'] == 'get': # We've something to send back. + # config_store_receiver expects json + cb_return = json.JSONEncoder().encode(cb_return) + conn.sendall( + cb_return.encode()) # No dispatch. Config store receiver will put to config store. + except (NameError, ValueError) as e: + self.log(f"Could not find register data for eid {eid}") + except OSError as e: + self.log(f"OSError: {e}") + raise + + def register(self, action: object, path: object, callback: object, *args: object) -> object: + if not self.running: + self.start() + # what about multiple registration? + eid = self.eids + self.eids += 1 + self.registry[eid] = {'cb': callback, 'action': action, 'path': path, 'args': args} + cmd = "register\n{}\n{}\n{}\n{}\n".format(self.pid, eid, action, path) + return self._dispatch(cmd) + + def unregister(self, eid): + ret = "" + try: + e = self.registry[eid] + except KeyError: + pass + else: + if self.running: + cmd = "unregister\n{}\n{}\n{}\n{}\n".format(self.pid, eid, e['action'], e['path']) + ret = self._dispatch(cmd) + del self.registry[eid] + return ret + + +def clean_up_reg(signal, frame): + """ + When 'cppython remote_port_forward.py' gets a SIGTERM, config_store_receiver.py doesn't + clean up registrations. Even if it did, the comm module can't rely on an external service + to clean up. + """ + EventingCSClient('CSClient').stop() + sys.exit(0) + + +signal.signal(signal.SIGTERM, clean_up_reg) diff --git a/appdata_sample/package.ini b/appdata_sample/package.ini new file mode 100644 index 00000000..7688877e --- /dev/null +++ b/appdata_sample/package.ini @@ -0,0 +1,13 @@ +[appdata_sample] +uuid = 0df5ee45-814d-451f-8888-87c2b05f2c3c +vendor = Cradlepoint +notes = Get SDK Appdata +version_major = 0 +version_minor = 1 +version_patch = 0 +auto_start = true +restart = true +reboot = true +firmware_major = 7 +firmware_minor = 24 + diff --git a/appdata_sample/readme.txt b/appdata_sample/readme.txt new file mode 100644 index 00000000..85ad0170 --- /dev/null +++ b/appdata_sample/readme.txt @@ -0,0 +1,26 @@ +Application Name +================ +appdata_sample + + +Application Version +=================== +0.1.0 + + +NCOS Devices Supported +====================== +ALL + + +External Requirements +===================== +None + + +Application Purpose +=================== +This is an example of how to use the SDK Appdata fields to store and retrieve data in NCOS configs. +The get_appdata() function will return the value of the appdata entry with the specified name. +If the appdata is not found, it will save the default_appdata to the NCOS Configs and return it. +The app runs a loop that logs the appdata every 10 seconds so you can see user changes. \ No newline at end of file diff --git a/appdata_sample/start.sh b/appdata_sample/start.sh new file mode 100644 index 00000000..035f7571 --- /dev/null +++ b/appdata_sample/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython appdata_sample.py \ No newline at end of file diff --git a/send_to_server/package.ini b/send_to_server/package.ini index 2a43c4ac..498405ec 100755 --- a/send_to_server/package.ini +++ b/send_to_server/package.ini @@ -3,9 +3,10 @@ uuid=82583a58-108e-4ac7-80e2-b0bba753330e vendor=Cradlepoint notes=Sends data to a server via http post. firmware_major=7 -firmware_minor=2 -restart=false +firmware_minor=24 +restart=true reboot=true -version_major=2 -version_minor=0 auto_start=true +version_major=2 +version_minor=1 +version_patch=0 diff --git a/send_to_server/readme.txt b/send_to_server/readme.txt index 721d845c..d8b7e669 100755 --- a/send_to_server/readme.txt +++ b/send_to_server/readme.txt @@ -5,7 +5,7 @@ send_to_server Application Version =================== -2.0 +2.1 NCOS Devices Supported @@ -20,12 +20,6 @@ This application uses test server http://httpbin.org/post. Application Purpose =================== -Demonstrates send a post request to a server with some device information -using python library urllib which is included in NCOS. - - -Expected Output -=============== -Data should be sent to test server http://httpbin.org/post. It also logs -the time it took to gather and send the data. - +App uses SDK Appdata to store settings for Server URL, Interval, and Payload. +Payload keys are field names and values are the NCOS path to get the data. +Data is collected and posted to server url every interval seconds. diff --git a/send_to_server/send_to_server.py b/send_to_server/send_to_server.py index 7f256c04..db7b73b7 100755 --- a/send_to_server/send_to_server.py +++ b/send_to_server/send_to_server.py @@ -1,53 +1,47 @@ -''' -Gets the '/status' from the router config store and send it -to a test server. -''' +# send_to_server - get data from user defined paths and POST to user defined server at user defined interval. +# Settings are stored in SDK Appdata. -import datetime -import urllib.request -import urllib.parse from csclient import EventingCSClient - -cp = EventingCSClient('send_to_server') - - -def post_to_server(): +import json +import time +import requests + +default_appdata = { + "server_url": "https://httpbin.org/post", # Server to POST to + "interval": 10, # Seconds between POST requests + "payload": { + # Identifiers: + "hostname": "config/system/system_id", + "mac": "status/product_info/mac0", + "serial_number": "status/product_info/manufacturing/serial_num", + # Data: + "gps": "status/gps/fix", + # "gpio": "status/gpio", + # "obd": "status/obd", + # "clients": "status/lan/clients", + } +} + +def get_appdata(name): + """Get appdata from NCOS Configs. If not found, save default_appdata and return it.""" try: - # The tree item to get from the router config store - tree_item = '/status/system/sdk' - start_time = datetime.datetime.now() - - # Get the item from the router config store - tree_data = cp.get(tree_item) - cp.log("{}: {}".format(tree_item, tree_data)) - - time_to_get = datetime.datetime.now() - start_time - encode_start_time = datetime.datetime.now() - - # URL encode the tree_data - params = urllib.parse.urlencode(tree_data) + appdata = cp.get('config/system/sdk/appdata') + data = json.loads([x["value"] for x in appdata if x["name"] == name][0]) + except: + data = default_appdata + cp.post('config/system/sdk/appdata', {"name": name, "value": json.dumps(data)}) + cp.log(f'No appdata found - Saved default: {data}') + return data - # UTF-8 encode the URL encoded data - params = params.encode('utf-8') - - time_to_encode = datetime.datetime.now() - encode_start_time - send_to_server_start_time = datetime.datetime.now() - - # Send a post request to a test server. It will respond with the data sent - # in the request - response = urllib.request.urlopen("http://httpbin.org/post", params) - end_time = datetime.datetime.now() - - # Log the response code and the processing timing information. - cp.log("data sent, http response code: {}".format(response.code)) - cp.log('Time to get data from router config store: {}'.format(time_to_get)) - cp.log('Time to urlencode data: {}'.format(time_to_encode)) - cp.log('Time to get reply from server: {}'.format(end_time - send_to_server_start_time)) - cp.log('Time to get and send data in post request: {}'.format(end_time - start_time)) - - except Exception as ex: - cp.log('Something went wrong! ex: {}'.format(ex)) - - -if __name__ == "__main__": - post_to_server() +cp = EventingCSClient('send_to_server') +cp.log('Starting...') + +while True: + config = get_appdata('send_to_server') + payload = {} + for key, path in config['payload'].items(): + payload[key] = cp.get(path) + cp.log(f'Payload: {payload}') + resp = requests.post(config['server_url'], json=payload) + cp.log(f'POST {config["server_url"]} {resp.status_code}') + time.sleep(config['interval'])