import json
import hashlib
from baboossh import Db
from baboossh.exceptions import NoPathError
from baboossh.utils import Unique
[docs]class Host(metaclass=Unique):
"""A machine with one or several :class:`Endpoint`
This is used to aggregate endpoints as a single machine can have several
interfaces with SSH listening on them. In order to prevent unecessary pivots,
:class:`Path` s are calculated using the `Host` as sources as it might be
longer to reach a `Host` from one endpoint rather than the other.
The aggregation is checked by :func:`Connection.identify`, which is run on
every endpoint newly connected. If every `Host` attribute matches with an
existing Host, the endpoint is considered to belong to it and is added.
Attributes:
name (str): the hostname of the Host as returned by the command `hostname`
id (int): the id of the Host
uname (str): the output of the command `uname -a` on the Host
issue (str): the content of the file `/etc/issue` on the Host
machine_id (str): the content of the file `/etc/machine-id` on the Host
macs ([str, ...]): a list of the MAC addresses of the Host interfaces
"""
search_fields = ['name', 'uname']
[docs] def __init__(self, hostname, uname, issue, machine_id, macs):
self.hostname = hostname
self.id = None
self.uname = uname
self.issue = issue
self.machine_id = machine_id
self.macs = macs
cursor = Db.get().cursor()
cursor.execute('SELECT id, name FROM hosts WHERE hostname=? AND uname=? AND issue=? AND machine_id=? AND macs=?', (self.hostname, self.uname, self.issue, self.machine_id, json.dumps(self.macs)))
saved_host = cursor.fetchone()
cursor.close()
if saved_host is not None:
self.id = saved_host[0]
self.name = saved_host[1]
else:
if hostname != "":
name = hostname.split(".")[0]
if len(name) > 20:
name = name[:20]
incr = 0
else:
name = "host"
incr = 1
self.name = None
while self.name is None:
fullname = name if incr == 0 else name+"_"+str(incr)
cursor = Db.get().cursor()
cursor.execute('SELECT id FROM hosts WHERE name=?', (fullname, ))
if cursor.fetchone() is not None:
incr = incr + 1
else:
self.name = fullname
cursor.close()
@classmethod
def get_id(cls, hostname, uname, issue, machine_id, macs):
return hashlib.sha256((hostname+uname+issue+machine_id+json.dumps(macs)).encode()).hexdigest()
@property
def scope(self):
"""Returns whether the `Host` is in scope
A `Host` is in scope if all its :class:`Endpoint` s are in scope
"""
for endpoint in self.endpoints:
if not endpoint.scope:
return False
return True
@scope.setter
def scope(self, scope):
for endpoint in self.endpoints:
endpoint.scope = scope
endpoint.save()
@property
def distance(self):
"""Returns the `Host` 's number of hops from `"Local"`"""
cursor = Db.get().cursor()
cursor.execute('SELECT distance FROM endpoints WHERE host=? ORDER BY distance DESC', (self.id, ))
row = cursor.fetchone()
cursor.close()
return row[0]
@property
def closest_endpoint(self):
"""Returns the `Host` 's closest :class:`Endpoint`"""
cursor = Db.get().cursor()
cursor.execute('SELECT ip, port FROM endpoints WHERE host=? ORDER BY distance DESC', (self.id, ))
row = cursor.fetchone()
cursor.close()
from baboossh import Endpoint
return Endpoint(row[0], row[1])
@property
def endpoints(self):
"""Returns a `List` of the `Host` 's :class:`Endpoint` s"""
from baboossh import Endpoint
endpoints = []
cursor = Db.get().cursor()
for row in cursor.execute('SELECT ip, port FROM endpoints WHERE host=?', (self.id, )):
endpoints.append(Endpoint(row[0], row[1]))
cursor.close()
return endpoints
[docs] def save(self):
"""Saves the `Host` in the :class:`Workspace` 's database"""
cursor = Db.get().cursor()
if self.id is not None:
#If we have an ID, the host is already saved in the database : UPDATE
cursor.execute('''UPDATE hosts
SET
name = ?,
hostname = ?,
uname = ?,
issue = ?,
machine_id = ?,
macs = ?
WHERE id = ?''',
(self.name, self.hostname, self.uname, self.issue, self.machine_id, json.dumps(self.macs), self.id))
else:
#The host doesn't exists in database : INSERT
cursor.execute('''INSERT INTO hosts(name, hostname, uname, issue, machine_id, macs)
VALUES (?, ?, ?, ?, ?, ?) ''',
(self.name, self.hostname, self.uname, self.issue, self.machine_id, json.dumps(self.macs)))
cursor.close()
cursor = Db.get().cursor()
cursor.execute('SELECT id FROM hosts WHERE name=?', (self.name, ))
self.id = cursor.fetchone()[0]
cursor.close()
Db.get().commit()
[docs] def delete(self):
"""Removes the `Host` from the :class:`Workspace`
Recursively removes all :class:`Path` s starting from this `Host`
"""
from baboossh import Path
if self.id is None:
return {}
from baboossh.utils import unstore_targets_merge
del_data = {}
for path in Path.find_all(src=self):
unstore_targets_merge(del_data, path.delete())
for endpoint in self.endpoints:
endpoint.host = None
endpoint.save()
cursor = Db.get().cursor()
cursor.execute('DELETE FROM hosts WHERE id = ?', (self.id, ))
cursor.close()
Db.get().commit()
unstore_targets_merge(del_data, {"Host":[type(self).get_id(self.hostname, self.uname, self.issue, self.machine_id, self.macs)]})
return del_data
[docs] @classmethod
def find_all(cls, scope=None):
"""Returns a `List` of all `Host` s in the :class:`Workspace` matching the criteria
Args:
scope (bool): whether to return only `Host`s in scope (`True`),
out of scope (`False`) or both (`None`)
name (str): the `Host` s' name to match
Returns:
the `List` of `Host` s
"""
ret = []
cursor = Db.get().cursor()
req = cursor.execute('SELECT hostname, uname, issue, machine_id, macs FROM hosts')
for row in req:
host = Host(row[0], row[1], row[2], row[3], json.loads(row[4]))
if scope is None:
ret.append(host)
elif host.scope == scope:
ret.append(host)
cursor.close()
return ret
[docs] @classmethod
def find_one(cls, host_id=None, name=None, prev_hop_to=None):
"""Find a `Host` by its id
Args:
host_id (int): the desired `Host` 's id
name (str): the `Host` 's name to match
Returns:
A `Host` or `None`
"""
if prev_hop_to is not None:
from baboossh import Path
paths = Path.find_all(dst=prev_hop_to)
smallest_distance = None
closest = None
for path in paths:
if path.src is None:
#Direct path found, we can stop here
return None
if closest is None:
closest = path.src
smallest_distance = path.src.distance
continue
if path.src.distance < smallest_distance:
closest = path.src
smallest_distance = path.src.distance
continue
if closest is None:
raise NoPathError
return closest
cursor = Db.get().cursor()
if host_id is not None:
cursor.execute('''SELECT hostname, uname, issue, machine_id, macs FROM hosts WHERE id=?''', (host_id, ))
elif name is not None:
cursor.execute('''SELECT hostname, uname, issue, machine_id, macs FROM hosts WHERE name=?''', (name, ))
else:
cursor.close()
return None
row = cursor.fetchone()
cursor.close()
if row is None:
return None
return Host(row[0], row[1], row[2], row[3], json.loads(row[4]))
[docs] @classmethod
def search(cls, field, val, show_all=False):
"""Search in the workspace for a `Host`
Args:
field (str): the `Host` attribute to search in
val (str): the value to search for
show_all (bool): whether to include out-of scope `Host` s in search results
Returns:
A `List` of `Host` s corresponding to the search.
"""
if field not in cls.search_fields:
raise ValueError
ret = []
cursor = Db.get().cursor()
val = "%"+val+"%"
#Ok this sounds fugly, but there seems to be no way to set a column name in a parameter. The SQL injection risk is mitigated as field must be in allowed fields, but if you find something better I take it
for row in cursor.execute('SELECT hostname, uname, issue, machine_id, macs FROM hosts WHERE {} LIKE ?'.format(field), (val, )):
ret.append(Host(row[0], row[1], row[2], row[3], json.loads(row[4])))
if not show_all:
ret = [host for host in ret if host.scope]
return ret
def __str__(self):
return self.name