Browse Source

Initial commit

master
Zack Marvel 2 years ago
commit
8e6005b7b0
  1. 124
      .gitignore
  2. 27
      README.md
  3. 13
      src/__init__.py
  4. 4
      src/__main__.py
  5. 22
      src/config.py
  6. 86
      src/monitor.py
  7. 15
      src/templates/index.html
  8. 12
      src/web.py

124
.gitignore

@ -0,0 +1,124 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

27
README.md

@ -0,0 +1,27 @@
# apt-monitor
apt-monitor provides a web interface to easily view which Aptitude-based systems
need package upgrades. It uses the [Paramiko][parakmiko] library to establish
an SSH connection to a list of systems, and the [Flask][flask] framework for
the web interface.
## Usage
apt-monitor should be provided with a list of systems to monitor, as well as an
SSH key associated with each system. It can be configured to poll each system
after a particular interval has elapsed.
## Considerations
- apt-monitor queries each system at a regular interval, and stores the list of
packages that need upgraded in memory. If you are using apt-monitor for a
particularly large number of systems, this could become a problem.
- This web interface **should not** be made publicly accesible. An attacker
could use it to determine that a vulnerable package is running on your
system.
[parakmiko]: http://www.paramiko.org/
[flask]: http://flask.pocoo.org/

13
src/__init__.py

@ -0,0 +1,13 @@
from .config import Config
from .monitor import Monitor
DEFAULT_CONFIG_PATH = 'apt-monitor.ini'
_config = Config(DEFAULT_CONFIG_PATH)
_monitor = Monitor(_config)
_monitor.start()

4
src/__main__.py

@ -0,0 +1,4 @@
from . import _monitor
_monitor.run()

22
src/config.py

@ -0,0 +1,22 @@
from configparser import ConfigParser
from pathlib import Path
class Config():
def __init__(self, path):
self.path = Path(path).resolve()
print('Attempting to open config {}'.format(path))
if not self.path.exists():
raise FileNotFoundError(path)
self.config = ConfigParser()
self.config.read(self.path)
def get_host(self, hostname):
return self.config[hostname]
def get_hosts(self):
return list(filter(lambda h: h != 'DEFAULTS' and h != 'apt-monitor', self.config.sections()))
def __getitem__(self, key):
return self.config[key]

86
src/monitor.py

@ -0,0 +1,86 @@
import threading
from time import sleep
import asyncssh
import asyncio
from pathlib import Path
# 5 minutes
DEFAULT_INTERVAL = 5*60
class Monitor(threading.Thread):
def __init__(self, config):
super().__init__()
self.config = config
# apt-monitor settings
app_config = config['apt-monitor']
if 'interval' in app_config:
self.interval = float(app_config['interval'])
else:
self.interval = DEFAULT_INTERVAL
# seconds
self.sleep_slice = 5
self.app_config = app_config
# default SSH client settings
self.defaults = config['DEFAULTS']
self._done = False
self._status = {}
def run(self):
while not self._done:
asyncio.run(self._run())
slept = 0
while slept < self.interval and not self._done:
sleep(self.sleep_slice)
slept += self.sleep_slice
def join(self):
self._done = True
super().join()
async def _run(self):
tasks = []
for host in self.config.get_hosts():
if host not in ['DEFAULTS', 'apt-monitor']:
tasks.append(self.check_host(host))
results = await asyncio.gather(*tasks, return_exceptions=True)
for (host, stdout, stderr) in results:
self._status[host] = list(
filter(lambda line: line != '', stdout))
print('HOST {} => {}'.format(host, stderr))
async def check_host(self, host):
if 'IdentityFile' in self.config[host]:
client_keys = [Path(self.config[host]['IdentityFile']).
expanduser()
.resolve()]
else:
client_keys = [Path(self.defaults['IdentityFile']).
expanduser().
resolve()]
if 'User' in self.config[host]:
username = self.config[host]
else:
username = self.defaults['User']
async with asyncssh.connect(host, username=username,
client_keys=client_keys) as conn:
cmd = '/usr/bin/apt list --upgradable'
print('Running {} on {}'.format(cmd, host))
upgradable_list = conn.run(cmd)
completed = await upgradable_list
if completed.exit_status == 0:
return (host, completed.stdout.splitlines(),
completed.stderr.splitlines())
else:
raise ConnectionError(host)
def get_status(self, host=None):
if host is not None:
return self._status[host]
else:
return self._status

15
src/templates/index.html

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<title>apt-monitor</title>
</head>
<body>
<h1>apt-monitor</h1>
<h2>Hosts</h2>
<ul>
{% for host in config.get_hosts() %}
<li><b>{{ host }}</b>: {{ monitor.get_status(host) }}</li>
{% endfor %}
</ul>
</body>
</html>

12
src/web.py

@ -0,0 +1,12 @@
from flask import Flask, render_template
from . import _monitor, _config
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', monitor=_monitor, config=_config)
Loading…
Cancel
Save