voixicron.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #! /usr/bin/env python3
  2. """
  3. This script checks for available updates on Void Linux using xbps and notifies
  4. the configured administrator account about them. It currently requires
  5. python 3.x and no additional packages and is supposed to be run with cron or
  6. another scheduled execution mechanism.
  7. """
  8. import subprocess
  9. import smtplib
  10. import socket
  11. import re
  12. import logging
  13. import sys
  14. from email.mime.multipart import MIMEMultipart
  15. from email.mime.text import MIMEText
  16. __version__ = "0.2"
  17. # Email server to be used to send the notification
  18. ADMIN_EMAIL = "root"
  19. # Mail server to be used to send the notification
  20. MAIL_SERVER = "localhost"
  21. # Port to use on the mail server
  22. MAIL_SERVER_PORT = 25
  23. # User name to log in to the mail server
  24. MAIL_SERVER_USER = ""
  25. # Password to log in to the mail server
  26. MAIL_SERVER_PASSWORD = ""
  27. # Log level for the stderr/stdout output
  28. LOG_LEVEL = logging.ERROR
  29. AVAILABLE_UPDATES = []
  30. DEVNULL = open("/dev/null", "w")
  31. LOGGER = logging.getLogger("voixicron")
  32. LOGGER.setLevel(LOG_LEVEL)
  33. CONSOLE_HANDLER = logging.StreamHandler()
  34. LOGGER.addHandler(CONSOLE_HANDLER)
  35. try:
  36. subprocess.call(["xbps-install", "-S"], stdout=DEVNULL, stderr=DEVNULL)
  37. except subprocess.CalledProcessError:
  38. LOGGER.warn("Repository information could not be updated, voixicron will "
  39. "report outdated data!")
  40. try:
  41. XBPS_INSTALL_RESULT = subprocess.check_output(["xbps-install", "-Sun"],
  42. stderr=DEVNULL)
  43. except subprocess.CalledProcessError as error:
  44. LOGGER.error("Could not get list of updated packages, xbps-install error:"
  45. " %s", error.output)
  46. sys.exit(1)
  47. else:
  48. XBPS_INSTALL_RESULT = XBPS_INSTALL_RESULT.decode("utf-8").split("\n")
  49. for line in XBPS_INSTALL_RESULT:
  50. if "update" in line:
  51. try:
  52. package = line.split(" ")[0]
  53. package_regex = "^([A-Za-z0-9-._+]*)-([0-9].*_[0-9]*)$"
  54. match_object = re.match(package_regex, package)
  55. if match_object:
  56. package_name = match_object.groups()[0]
  57. new_package_version = match_object.groups()[1]
  58. else:
  59. LOGGER.error("Not match for package %s", package)
  60. continue
  61. except IndexError:
  62. continue
  63. else:
  64. try:
  65. xbps_query_result = subprocess.check_output(
  66. ["xbps-query", "--show", package_name], stderr=DEVNULL)
  67. except subprocess.CalledProcessError as error:
  68. LOGGER.debug("Querying the installed version of package %s"
  69. " failed with error message: %s",
  70. package_name, error.output)
  71. else:
  72. xbps_query_result = xbps_query_result.decode(
  73. "utf-8").split("\n")
  74. for line in xbps_query_result:
  75. if "pkgver" in line:
  76. installed_package = line.split(
  77. ":")[1].strip()
  78. match = re.match(package_regex,
  79. installed_package)
  80. if match:
  81. installed_package_version = match.groups()[1]
  82. package_update = {
  83. "name": package_name,
  84. "new_version": new_package_version,
  85. "old_version": installed_package_version,
  86. }
  87. else:
  88. LOGGER.error("Unable to detect installed "
  89. "version of package %s",
  90. package_name)
  91. package_update = {
  92. "name": package_name,
  93. "new_version": new_package_version,
  94. "old_version": "unknown",
  95. }
  96. AVAILABLE_UPDATES.append(package_update)
  97. break
  98. if AVAILABLE_UPDATES:
  99. HOSTNAME = socket.getfqdn()
  100. MSG = MIMEMultipart()
  101. MAIL_TEXT = "The following package updates are available on host %s:\n\n"\
  102. % HOSTNAME
  103. MSG["Subject"] = "%s updates available on host %s"\
  104. % (len(AVAILABLE_UPDATES), HOSTNAME)
  105. MSG["From"] = "voixicron@%s" % HOSTNAME
  106. MSG["To"] = ADMIN_EMAIL
  107. for update in AVAILABLE_UPDATES:
  108. MAIL_TEXT += "%s (%s -> %s)\n"\
  109. % (update["name"],
  110. update["old_version"],
  111. update["new_version"])
  112. MAIL_TEXT += "\n\nYou can install the updates by issuing the command:" \
  113. "\n\n\txbps-install -Su\n\nas root on %s\n\n--\nvoixicron"\
  114. % HOSTNAME
  115. MSG_TEXT = MIMEText(MAIL_TEXT.encode("utf-8"), _charset="utf-8")
  116. MSG.attach(MSG_TEXT)
  117. SERVER_CONNECTION = smtplib.SMTP(MAIL_SERVER, MAIL_SERVER_PORT)
  118. try:
  119. SERVER_CONNECTION.connect()
  120. except (smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected,
  121. socket.error):
  122. LOGGER.error("Failed to establish a connection to the SMTP server")
  123. else:
  124. try:
  125. SERVER_CONNECTION.starttls()
  126. except smtplib.SMTPException:
  127. pass
  128. if MAIL_SERVER_USER:
  129. try:
  130. SERVER_CONNECTION.login(MAIL_SERVER_USER, MAIL_SERVER_PASSWORD)
  131. except smtplib.SMTPAuthenticationError:
  132. LOGGER.error("Authentication on the mail server with user %s "
  133. "failed.", MAIL_SERVER_USER)
  134. except smtplib.SMTPException:
  135. LOGGER.error("Error during authentication on the mail server.")
  136. try:
  137. SERVER_CONNECTION.sendmail("voixicron@%s" % HOSTNAME, ADMIN_EMAIL,
  138. MSG.as_string())
  139. except smtplib.SMTPRecipientsRefused:
  140. LOGGER.error("The mail server refused the recipient.")
  141. except smtplib.SMTPSenderRefused:
  142. LOGGER.error("The mail server refused the sender.")
  143. else:
  144. SERVER_CONNECTION.quit()
  145. DEVNULL.close()