Browse Source

Initial commit

master
Zack Marvel 1 year ago
commit
8e6005b7b0
8 changed files with 303 additions and 0 deletions
  1. +124
    -0
      .gitignore
  2. +27
    -0
      README.md
  3. +13
    -0
      src/__init__.py
  4. +4
    -0
      src/__main__.py
  5. +22
    -0
      src/config.py
  6. +86
    -0
      src/monitor.py
  7. +15
    -0
      src/templates/index.html
  8. +12
    -0
      src/web.py

+ 124
- 0
.gitignore View File

@@ -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
- 0
README.md View File

@@ -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
- 0
src/__init__.py View File

@@ -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
- 0
src/__main__.py View File

@@ -0,0 +1,4 @@

from . import _monitor

_monitor.run()

+ 22
- 0
src/config.py View File

@@ -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
- 0
src/monitor.py View File

@@ -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
- 0
src/templates/index.html View File

@@ -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
- 0
src/web.py View File

@@ -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