helpers.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import datetime
  4. from enum import Enum
  5. from importlib import util
  6. import platform
  7. import io
  8. import json
  9. import logging
  10. import netifaces
  11. from OpenSSL import crypto
  12. import os
  13. from pathlib import Path
  14. import subprocess
  15. import urllib3
  16. import zipfile
  17. from threading import Thread
  18. import time
  19. import contextlib
  20. import requests
  21. import secrets
  22. from odoo import _, http, service
  23. from odoo.tools.func import lazy_property
  24. from odoo.modules.module import get_resource_path
  25. _logger = logging.getLogger(__name__)
  26. try:
  27. import crypt
  28. except ImportError:
  29. _logger.warning('Could not import library crypt')
  30. #----------------------------------------------------------
  31. # Helper
  32. #----------------------------------------------------------
  33. class CertificateStatus(Enum):
  34. OK = 1
  35. NEED_REFRESH = 2
  36. ERROR = 3
  37. class IoTRestart(Thread):
  38. """
  39. Thread to restart odoo server in IoT Box when we must return a answer before
  40. """
  41. def __init__(self, delay):
  42. Thread.__init__(self)
  43. self.delay = delay
  44. def run(self):
  45. time.sleep(self.delay)
  46. service.server.restart()
  47. if platform.system() == 'Windows':
  48. writable = contextlib.nullcontext
  49. elif platform.system() == 'Linux':
  50. @contextlib.contextmanager
  51. def writable():
  52. subprocess.call(["sudo", "mount", "-o", "remount,rw", "/"])
  53. subprocess.call(["sudo", "mount", "-o", "remount,rw", "/root_bypass_ramdisks/"])
  54. try:
  55. yield
  56. finally:
  57. subprocess.call(["sudo", "mount", "-o", "remount,ro", "/"])
  58. subprocess.call(["sudo", "mount", "-o", "remount,ro", "/root_bypass_ramdisks/"])
  59. subprocess.call(["sudo", "mount", "-o", "remount,rw", "/root_bypass_ramdisks/etc/cups"])
  60. def access_point():
  61. return get_ip() == '10.11.12.1'
  62. def start_nginx_server():
  63. if platform.system() == 'Windows':
  64. path_nginx = get_path_nginx()
  65. if path_nginx:
  66. os.chdir(path_nginx)
  67. _logger.info('Start Nginx server: %s\\nginx.exe', path_nginx)
  68. os.popen('nginx.exe')
  69. os.chdir('..\\server')
  70. elif platform.system() == 'Linux':
  71. subprocess.check_call(["sudo", "service", "nginx", "restart"])
  72. def check_certificate():
  73. """
  74. Check if the current certificate is up to date or not authenticated
  75. :return CheckCertificateStatus
  76. """
  77. server = get_odoo_server_url()
  78. if not server:
  79. return {"status": CertificateStatus.ERROR,
  80. "error_code": "ERR_IOT_HTTPS_CHECK_NO_SERVER"}
  81. if platform.system() == 'Windows':
  82. path = Path(get_path_nginx()).joinpath('conf/nginx-cert.crt')
  83. elif platform.system() == 'Linux':
  84. path = Path('/etc/ssl/certs/nginx-cert.crt')
  85. if not path.exists():
  86. return {"status": CertificateStatus.NEED_REFRESH}
  87. try:
  88. with path.open('r') as f:
  89. cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
  90. except EnvironmentError:
  91. _logger.exception("Unable to read certificate file")
  92. return {"status": CertificateStatus.ERROR,
  93. "error_code": "ERR_IOT_HTTPS_CHECK_CERT_READ_EXCEPTION"}
  94. cert_end_date = datetime.datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") - datetime.timedelta(days=10)
  95. for key in cert.get_subject().get_components():
  96. if key[0] == b'CN':
  97. cn = key[1].decode('utf-8')
  98. if cn == 'OdooTempIoTBoxCertificate' or datetime.datetime.now() > cert_end_date:
  99. message = _('Your certificate %s must be updated') % (cn)
  100. _logger.info(message)
  101. return {"status": CertificateStatus.NEED_REFRESH}
  102. else:
  103. message = _('Your certificate %s is valid until %s') % (cn, cert_end_date)
  104. _logger.info(message)
  105. return {"status": CertificateStatus.OK, "message": message}
  106. def check_git_branch():
  107. """
  108. Check if the local branch is the same than the connected Odoo DB and
  109. checkout to match it if needed.
  110. """
  111. server = get_odoo_server_url()
  112. if server:
  113. urllib3.disable_warnings()
  114. http = urllib3.PoolManager(cert_reqs='CERT_NONE')
  115. try:
  116. response = http.request(
  117. 'POST',
  118. server + "/web/webclient/version_info",
  119. body = '{}',
  120. headers = {'Content-type': 'application/json'}
  121. )
  122. if response.status == 200:
  123. git = ['git', '--work-tree=/home/pi/odoo/', '--git-dir=/home/pi/odoo/.git']
  124. db_branch = json.loads(response.data)['result']['server_serie'].replace('~', '-')
  125. if not subprocess.check_output(git + ['ls-remote', 'origin', db_branch]):
  126. db_branch = 'master'
  127. local_branch = subprocess.check_output(git + ['symbolic-ref', '-q', '--short', 'HEAD']).decode('utf-8').rstrip()
  128. if db_branch != local_branch:
  129. with writable():
  130. subprocess.check_call(["rm", "-rf", "/home/pi/odoo/addons/hw_drivers/iot_handlers/drivers/*"])
  131. subprocess.check_call(["rm", "-rf", "/home/pi/odoo/addons/hw_drivers/iot_handlers/interfaces/*"])
  132. subprocess.check_call(git + ['branch', '-m', db_branch])
  133. subprocess.check_call(git + ['remote', 'set-branches', 'origin', db_branch])
  134. os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/posbox_update.sh')
  135. except Exception as e:
  136. _logger.error('Could not reach configured server')
  137. _logger.error('A error encountered : %s ' % e)
  138. def check_image():
  139. """
  140. Check if the current image of IoT Box is up to date
  141. """
  142. url = 'https://nightly.odoo.com/master/iotbox/SHA1SUMS.txt'
  143. urllib3.disable_warnings()
  144. http = urllib3.PoolManager(cert_reqs='CERT_NONE')
  145. response = http.request('GET', url)
  146. checkFile = {}
  147. valueActual = ''
  148. for line in response.data.decode().split('\n'):
  149. if line:
  150. value, name = line.split(' ')
  151. checkFile.update({value: name})
  152. if name == 'iotbox-latest.zip':
  153. valueLastest = value
  154. elif name == get_img_name():
  155. valueActual = value
  156. if valueActual == valueLastest:
  157. return False
  158. version = checkFile.get(valueLastest, 'Error').replace('iotboxv', '').replace('.zip', '').split('_')
  159. return {'major': version[0], 'minor': version[1]}
  160. def save_conf_server(url, token, db_uuid, enterprise_code):
  161. """
  162. Save config to connect IoT to the server
  163. """
  164. write_file('odoo-remote-server.conf', url)
  165. write_file('token', token)
  166. write_file('odoo-db-uuid.conf', db_uuid or '')
  167. write_file('odoo-enterprise-code.conf', enterprise_code or '')
  168. def generate_password():
  169. """
  170. Generate an unique code to secure raspberry pi
  171. """
  172. alphabet = 'abcdefghijkmnpqrstuvwxyz23456789'
  173. password = ''.join(secrets.choice(alphabet) for i in range(12))
  174. shadow_password = crypt.crypt(password, crypt.mksalt())
  175. subprocess.call(('sudo', 'usermod', '-p', shadow_password, 'pi'))
  176. with writable():
  177. subprocess.call(('sudo', 'cp', '/etc/shadow', '/root_bypass_ramdisks/etc/shadow'))
  178. return password
  179. def get_certificate_status(is_first=True):
  180. """
  181. Will get the HTTPS certificate details if present. Will load the certificate if missing.
  182. :param is_first: Use to make sure that the recursion happens only once
  183. :return: (bool, str)
  184. """
  185. check_certificate_result = check_certificate()
  186. certificateStatus = check_certificate_result["status"]
  187. if certificateStatus == CertificateStatus.ERROR:
  188. return False, check_certificate_result["error_code"]
  189. if certificateStatus == CertificateStatus.NEED_REFRESH and is_first:
  190. certificate_process = load_certificate()
  191. if certificate_process is not True:
  192. return False, certificate_process
  193. return get_certificate_status(is_first=False) # recursive call to attempt certificate read
  194. return True, check_certificate_result.get("message",
  195. "The HTTPS certificate was generated correctly")
  196. def get_img_name():
  197. major, minor = get_version().split('.')
  198. return 'iotboxv%s_%s.zip' % (major, minor)
  199. def get_ip():
  200. interfaces = netifaces.interfaces()
  201. for interface in interfaces:
  202. if netifaces.ifaddresses(interface).get(netifaces.AF_INET):
  203. addr = netifaces.ifaddresses(interface).get(netifaces.AF_INET)[0]['addr']
  204. if addr != '127.0.0.1':
  205. return addr
  206. def get_mac_address():
  207. interfaces = netifaces.interfaces()
  208. for interface in interfaces:
  209. if netifaces.ifaddresses(interface).get(netifaces.AF_INET):
  210. addr = netifaces.ifaddresses(interface).get(netifaces.AF_LINK)[0]['addr']
  211. if addr != '00:00:00:00:00:00':
  212. return addr
  213. def get_path_nginx():
  214. return str(list(Path().absolute().parent.glob('*nginx*'))[0])
  215. def get_ssid():
  216. ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
  217. if not ap:
  218. return subprocess.check_output(['grep', '-oP', '(?<=ssid=).*', '/etc/hostapd/hostapd.conf']).decode('utf-8').rstrip()
  219. process_iwconfig = subprocess.Popen(['iwconfig'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  220. process_grep = subprocess.Popen(['grep', 'ESSID:"'], stdin=process_iwconfig.stdout, stdout=subprocess.PIPE)
  221. return subprocess.check_output(['sed', 's/.*"\\(.*\\)"/\\1/'], stdin=process_grep.stdout).decode('utf-8').rstrip()
  222. def get_odoo_server_url():
  223. if platform.system() == 'Linux':
  224. ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
  225. if not ap:
  226. return False
  227. return read_file_first_line('odoo-remote-server.conf')
  228. def get_token():
  229. return read_file_first_line('token')
  230. def get_version():
  231. if platform.system() == 'Linux':
  232. return read_file_first_line('/var/odoo/iotbox_version')
  233. elif platform.system() == 'Windows':
  234. return 'W22_11'
  235. def get_wifi_essid():
  236. wifi_options = []
  237. process_iwlist = subprocess.Popen(['sudo', 'iwlist', 'wlan0', 'scan'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  238. process_grep = subprocess.Popen(['grep', 'ESSID:"'], stdin=process_iwlist.stdout, stdout=subprocess.PIPE).stdout.readlines()
  239. for ssid in process_grep:
  240. essid = ssid.decode('utf-8').split('"')[1]
  241. if essid not in wifi_options:
  242. wifi_options.append(essid)
  243. return wifi_options
  244. def load_certificate():
  245. """
  246. Send a request to Odoo with customer db_uuid and enterprise_code to get a true certificate
  247. """
  248. db_uuid = read_file_first_line('odoo-db-uuid.conf')
  249. enterprise_code = read_file_first_line('odoo-enterprise-code.conf')
  250. if not (db_uuid and enterprise_code):
  251. return "ERR_IOT_HTTPS_LOAD_NO_CREDENTIAL"
  252. url = 'https://www.odoo.com/odoo-enterprise/iot/x509'
  253. data = {
  254. 'params': {
  255. 'db_uuid': db_uuid,
  256. 'enterprise_code': enterprise_code
  257. }
  258. }
  259. urllib3.disable_warnings()
  260. http = urllib3.PoolManager(cert_reqs='CERT_NONE', retries=urllib3.Retry(4))
  261. try:
  262. response = http.request(
  263. 'POST',
  264. url,
  265. body = json.dumps(data).encode('utf8'),
  266. headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
  267. )
  268. except Exception as e:
  269. _logger.exception("An error occurred while trying to reach odoo.com servers.")
  270. return "ERR_IOT_HTTPS_LOAD_REQUEST_EXCEPTION\n\n%s" % e
  271. if response.status != 200:
  272. return "ERR_IOT_HTTPS_LOAD_REQUEST_STATUS %s\n\n%s" % (response.status, response.reason)
  273. result = json.loads(response.data.decode('utf8'))['result']
  274. if not result:
  275. return "ERR_IOT_HTTPS_LOAD_REQUEST_NO_RESULT"
  276. write_file('odoo-subject.conf', result['subject_cn'])
  277. if platform.system() == 'Linux':
  278. with writable():
  279. Path('/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem'])
  280. Path('/root_bypass_ramdisks/etc/ssl/certs/nginx-cert.crt').write_text(result['x509_pem'])
  281. Path('/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem'])
  282. Path('/root_bypass_ramdisks/etc/ssl/private/nginx-cert.key').write_text(result['private_key_pem'])
  283. elif platform.system() == 'Windows':
  284. Path(get_path_nginx()).joinpath('conf/nginx-cert.crt').write_text(result['x509_pem'])
  285. Path(get_path_nginx()).joinpath('conf/nginx-cert.key').write_text(result['private_key_pem'])
  286. time.sleep(3)
  287. if platform.system() == 'Windows':
  288. odoo_restart(0)
  289. elif platform.system() == 'Linux':
  290. start_nginx_server()
  291. return True
  292. def download_iot_handlers(auto=True):
  293. """
  294. Get the drivers from the configured Odoo server
  295. """
  296. server = get_odoo_server_url()
  297. if server:
  298. urllib3.disable_warnings()
  299. pm = urllib3.PoolManager(cert_reqs='CERT_NONE')
  300. server = server + '/iot/get_handlers'
  301. try:
  302. resp = pm.request('POST', server, fields={'mac': get_mac_address(), 'auto': auto}, timeout=8)
  303. if resp.data:
  304. with writable():
  305. drivers_path = ['odoo', 'addons', 'hw_drivers', 'iot_handlers']
  306. path = path_file(str(Path().joinpath(*drivers_path)))
  307. zip_file = zipfile.ZipFile(io.BytesIO(resp.data))
  308. zip_file.extractall(path)
  309. except Exception as e:
  310. _logger.error('Could not reach configured server')
  311. _logger.error('A error encountered : %s ' % e)
  312. def load_iot_handlers():
  313. """
  314. This method loads local files: 'odoo/addons/hw_drivers/iot_handlers/drivers' and
  315. 'odoo/addons/hw_drivers/iot_handlers/interfaces'
  316. And execute these python drivers and interfaces
  317. """
  318. for directory in ['interfaces', 'drivers']:
  319. path = get_resource_path('hw_drivers', 'iot_handlers', directory)
  320. filesList = list_file_by_os(path)
  321. for file in filesList:
  322. spec = util.spec_from_file_location(file, str(Path(path).joinpath(file)))
  323. if spec:
  324. module = util.module_from_spec(spec)
  325. try:
  326. spec.loader.exec_module(module)
  327. except Exception as e:
  328. _logger.error('Unable to load file: %s ', file)
  329. _logger.error('An error encountered : %s ', e)
  330. lazy_property.reset_all(http.root)
  331. def list_file_by_os(file_list):
  332. platform_os = platform.system()
  333. if platform_os == 'Linux':
  334. return [x.name for x in Path(file_list).glob('*[!W].*')]
  335. elif platform_os == 'Windows':
  336. return [x.name for x in Path(file_list).glob('*[!L].*')]
  337. def odoo_restart(delay):
  338. IR = IoTRestart(delay)
  339. IR.start()
  340. def path_file(filename):
  341. platform_os = platform.system()
  342. if platform_os == 'Linux':
  343. return Path.home() / filename
  344. elif platform_os == 'Windows':
  345. return Path().absolute().parent.joinpath('server/' + filename)
  346. def read_file_first_line(filename):
  347. path = path_file(filename)
  348. if path.exists():
  349. with path.open('r') as f:
  350. return f.readline().strip('\n')
  351. def unlink_file(filename):
  352. with writable():
  353. path = path_file(filename)
  354. if path.exists():
  355. path.unlink()
  356. def write_file(filename, text, mode='w'):
  357. with writable():
  358. path = path_file(filename)
  359. with open(path, mode) as f:
  360. f.write(text)
  361. def download_from_url(download_url, path_to_filename):
  362. """
  363. This function downloads from its 'download_url' argument and
  364. saves the result in 'path_to_filename' file
  365. The 'path_to_filename' needs to be a valid path + file name
  366. (Example: 'C:\\Program Files\\Odoo\\downloaded_file.zip')
  367. """
  368. try:
  369. request_response = requests.get(download_url, timeout=60)
  370. request_response.raise_for_status()
  371. write_file(path_to_filename, request_response.content, 'wb')
  372. _logger.info('Downloaded %s from %s', path_to_filename, download_url)
  373. except Exception as e:
  374. _logger.error('Failed to download from %s: %s', download_url, e)
  375. def unzip_file(path_to_filename, path_to_extract):
  376. """
  377. This function unzips 'path_to_filename' argument to
  378. the path specified by 'path_to_extract' argument
  379. and deletes the originally used .zip file
  380. Example: unzip_file('C:\\Program Files\\Odoo\\downloaded_file.zip', 'C:\\Program Files\\Odoo\\new_folder'))
  381. Will extract all the contents of 'downloaded_file.zip' to the 'new_folder' location)
  382. """
  383. try:
  384. with writable():
  385. path = path_file(path_to_filename)
  386. with zipfile.ZipFile(path) as zip_file:
  387. zip_file.extractall(path_file(path_to_extract))
  388. Path(path).unlink()
  389. _logger.info('Unzipped %s to %s', path_to_filename, path_to_extract)
  390. except Exception as e:
  391. _logger.error('Failed to unzip %s: %s', path_to_filename, e)