"""Module containing multiple classes to obtain QNAP system stats via cgi calls.""" # -*- coding:utf-8 -*- import base64 import json import xmltodict import requests # pylint: disable=too-many-instance-attributes class QNAPStats: """Class containing the main functions.""" # pylint: disable=too-many-arguments def __init__(self, host, port, username, password, debugmode=False, verify_ssl=True, timeout=5): """Instantiate a new qnap_stats object.""" self._username = username self._password = base64.b64encode(password.encode('utf-8')).decode('ascii') self._sid = None self._debugmode = debugmode self._session_error = False self._session = None # type: requests.Session if not (host.startswith("http://") or host.startswith("https://")): host = "http://" + host self._verify_ssl = verify_ssl self._timeout = timeout self._base_url = '%s:%s/cgi-bin/' % (host, port) def _debuglog(self, message): """Output message if debug mode is enabled.""" if self._debugmode: print("DEBUG: " + message) def _init_session(self): if self._sid is None or self._session is None or self._session_error: # Clear sid and reset error self._sid = None self._session_error = False if self._session is not None: self._session = None self._debuglog("Creating new session") self._session = requests.Session() # We created a new session so login if self._login() is False: self._session_error = True self._debuglog("Login failed, unable to process request") return def _login(self): """Log into QNAP and obtain a session id.""" data = {"user": self._username, "pwd": self._password} result = self._execute_post_url("authLogin.cgi", data, False) if result is None or not result.get("authSid"): # Another method to login suffix_url = "authLogin.cgi?user=" + self._username + "&pwd=" + self._password result = self._execute_get_url(suffix_url, False) if result is None: return False self._sid = result["authSid"] return True def _get_url(self, url, retry_on_error=True, **kwargs): """High-level function for making GET requests.""" self._init_session() result = self._execute_get_url(url, **kwargs) if (self._session_error or result is None) and retry_on_error: self._debuglog("Error occured, retrying...") self._get_url(url, False, **kwargs) return result def _execute_get_url(self, url, append_sid=True, **kwargs): """Low-level function to execute a GET request.""" url = self._base_url + url self._debuglog("GET from URL: " + url) if append_sid: self._debuglog("Appending access_token (SID: " + self._sid + ") to url") url = "%s&sid=%s" % (url, self._sid) resp = self._session.get(url, timeout=self._timeout, verify=self._verify_ssl) return self._handle_response(resp, **kwargs) def _execute_post_url(self, url, data, append_sid=True, **kwargs): """Low-level function to execute a POST request.""" url = self._base_url + url self._debuglog("POST to URL: " + url) if append_sid: self._debuglog("Appending access_token (SID: " + self._sid + ") to url") data["sid"] = self._sid resp = self._session.post(url, data, timeout=self._timeout, verify=self._verify_ssl) return self._handle_response(resp, **kwargs) def _handle_response(self, resp, force_list=None): """Ensure response is successful and return body as XML.""" self._debuglog("Request executed: " + str(resp.status_code)) if resp.status_code != 200: return None if resp.headers["Content-Type"] != "text/xml": # JSON requests not currently supported return None self._debuglog("Headers: " + json.dumps(dict(resp.headers))) self._debuglog("Cookies: " + json.dumps(dict(resp.cookies))) self._debuglog("Response Text: " + resp.text) data = xmltodict.parse(resp.content, force_list=force_list)['QDocRoot'] auth_passed = data.get('authPassed') if auth_passed is not None and len(auth_passed) == 1 and auth_passed == "0": self._session_error = True return None return data def get_system_health(self): """Obtain the system's overall health.""" resp = self._get_url("management/manaRequest.cgi?subfunc=sysinfo&sysHealth=1") if resp is None: return None status = resp["func"]["ownContent"]["sysHealth"]["status"] if status is None or len(status) == 0: return None return status def get_volumes(self): """Obtain information about volumes and shared directories.""" resp = self._get_url( "management/chartReq.cgi?chart_func=disk_usage&disk_select=all&include=all", force_list=("volume", "volumeUse", "folder_element") ) if resp is None: return None if resp["volumeList"] is None or resp["volumeUseList"] is None: return {} volumes = {} id_map = {} for vol in resp["volumeList"]["volume"]: key = vol["volumeValue"] label = vol["volumeLabel"] if "volumeLabel" in vol else "Volume " + vol["volumeValue"] volumes[label] = { "id": key, "label": label } id_map[key] = label for vol in resp["volumeUseList"]["volumeUse"]: id_number = vol["volumeValue"] # Skip any system reserved volumes if id_number not in id_map.keys(): continue key = id_map[id_number] volumes[key]["free_size"] = int(vol["free_size"]) volumes[key]["total_size"] = int(vol["total_size"]) folder_elements = vol["folder_element"] if len(folder_elements) > 0: volumes[key]["folders"] = [] for folder in folder_elements: try: sharename = folder["sharename"] used_size = int(folder["used_size"]) volumes[key]["folders"].append({"sharename": sharename, "used_size": used_size}) except Exception as e: print(e.args) return volumes def get_smart_disk_health(self): """Obtain SMART information about each disk.""" resp = self._get_url("disk/qsmart.cgi?func=all_hd_data", force_list=("entry")) if resp is None: return None disks = {} for disk in resp["Disk_Info"]["entry"]: if disk["Model"]: disks[disk["HDNo"]] = { "drive_number": disk["HDNo"], "health": disk["Health"], "temp_c": int(disk["Temperature"]["oC"]) if disk["Temperature"]["oC"] is not None else None, "temp_f": int(disk["Temperature"]["oF"]) if disk["Temperature"]["oF"] is not None else None, "capacity": disk["Capacity"], "model": disk["Model"], "serial": disk["Serial"], "type": "ssd" if ("hd_is_ssd" in disk and int(disk["hd_is_ssd"])) else "hdd", } return disks def get_system_stats(self): """Obtain core system information and resource utilization.""" resp = self._get_url( "management/manaRequest.cgi?subfunc=sysinfo&hd=no&multicpu=1", force_list=("DNS_LIST") ) if resp is None: return None root = resp["func"]["ownContent"]["root"] details = { "system": { "name": root["server_name"], "model": resp["model"]["displayModelName"], "serial_number": root["serial_number"], "temp_c": int(root["sys_tempc"]), "temp_f": int(root["sys_tempf"]), "timezone": root["timezone"], }, "firmware": { "version": resp["firmware"]["version"], "build": resp["firmware"]["build"], "patch": resp["firmware"]["patch"], "build_time": resp["firmware"]["buildTime"], }, "uptime": { "days": int(root["uptime_day"]), "hours": int(root["uptime_hour"]), "minutes": int(root["uptime_min"]), "seconds": int(root["uptime_sec"]), }, "cpu": { "model": root["cpu_model"] if "cpu_model" in root else None, "usage_percent": float(root["cpu_usage"].replace("%", "")), "temp_c": int(root["cpu_tempc"]) if "cpu_tempc" in root else None, "temp_f": int(root["cpu_tempf"]) if "cpu_tempf" in root else None, }, "memory": { "total": float(root["total_memory"]), "free": float(root["free_memory"]), }, "nics": {}, "dns": [], } nic_count = int(root["nic_cnt"]) for nic_index in range(nic_count): i = str(nic_index + 1) interface = "eth" + str(nic_index) status = root["eth_status" + i] details["nics"][interface] = { "link_status": "Up" if status == "1" else "Down", "max_speed": int(root["eth_max_speed" + i]), "ip": root["eth_ip" + i], "mask": root["eth_mask" + i], "mac": root["eth_mac" + i], "usage": root["eth_usage" + i], "rx_packets": int(root["rx_packet" + i]), "tx_packets": int(root["tx_packet" + i]), "err_packets": int(root["err_packet" + i]) } dnsInfo = root.get("dnsInfo") if dnsInfo: for dns in dnsInfo["DNS_LIST"]: details["dns"].append(dns) return details def get_bandwidth(self): """Obtain the current bandwidth usage speeds.""" resp = self._get_url( "management/chartReq.cgi?chart_func=QSM40bandwidth", force_list="item" ) if resp and "bandwidth_info" not in resp: # changes in API since QTS 4.5.4, old query returns no values resp = self._get_url("management/chartReq.cgi?chart_func=bandwidth") if resp is None: return None details = {} interfaces = [] bandwidth_info = resp["bandwidth_info"] default = resp.get("df_gateway") or bandwidth_info.get("df_gateway") if "item" in bandwidth_info: interfaces.extend(bandwidth_info["item"]) else: interfaceIds = [] if bandwidth_info["eth_index_list"]: for num in bandwidth_info["eth_index_list"].split(','): interfaceIds.append("eth" + num) if bandwidth_info["wlan_index_list"]: for num in bandwidth_info["wlan_index_list"].split(','): interfaceIds.append("wlan" + num) for interfaceId in interfaceIds: interface = bandwidth_info[interfaceId] interface["id"] = interfaceId interfaces.extend([interface]) for item in interfaces: details[item["id"]] = { "name": item["dname"] if "dname" in item else item["name"], "rx": round(int(item["rx"]) / 5), "tx": round(int(item["tx"]) / 5), "is_default": item["id"] == default } return details def get_firmware_update(self): """Get firmware update version if available.""" resp = self._get_url("sys/sysRequest.cgi?subfunc=firm_update") if resp is None: return None new_version = resp["func"]["ownContent"]["newVersion"] if new_version is None or len(new_version) == 0: return None return new_version