Building VM Inventory #1: Paramiko
Sunday, 25 August 2024 01:13:44 WIB | tags: tips, python, gcp, vm-inventory | 219 hits | 0 comment(s)Ever wondered how to pull together a VM inventory when you've got a bunch of VMs? I’ve found myself asking this question a few times, so I decided to check out a few tools that might help. This post is the first in a series where I'll dive into different ways to gather basic VM info, like hostname and OS version. In this first post, I will use Paramiko library in Python application and see how I can use this to solve my problem. In the exploration, I will deploy 3 VMs in a cloud environment, which consists of one main server and two client servers.
Paramiko is a Python library that makes it easier to establish SSH connections across multiple clients, allowing you to automate tasks on remote servers. With Paramiko, you can programmatically connect to a server, execute commands, transfer files, and manage SSH keys without needing to manually interact with the server.
Let’s start with deploying the VMs. I use my personal GCP account and start fresh. Below are the steps for preparing the GCP environment. You can follow these steps if you want to prepare your own GCP environment. You can skip this section and directly go to Schrader - My Own Paramiko Wrapper section if you don’t.
Contents
Schrader - My Own Paramiko Wrapper
Preparing GCP Environment
Enable Compute Engine API
Ensure the Compute Engine API
is enabled in your GCP account to use the features.
Remove the default VPC network
I want to use my own VPC network so I will remove the default
VPC network. By default, this VPC network will create subnets in all available regions.
Create custom VPC network
I will create one subnet in the VPC network, in Singapore region (asia-southeast1
). While for the IPv4 range, I use a simple one: 10.0.0.0/24
. Once the VPC network and subnet are created, they will be visible under the Networks in Current Project and Subnets in Current Project tabs.
Deploy Main VM
In this step, I create a main server which I call remote-server
. I use the cheapest machine configuration, which is ec2-micro
with 2 vCPU 1 shared core and 1GB of memory. For the main server, I used Ubuntu 22.04 LTS
image and I also applied a network tag, which I named similar to the VM’s name: remote-server
. This network tag will be used to specify a target in my firewall rule. Lastly, I will keep the default ephemeral Public IP since I will need to access this VM from my local machine and set the network service tier to standard.
Deploy Client VMs
To demonstrate the multiple VMs that are being gathered by our main VM, I created 2 VMs with specifications:
-
remove-client-1 *I mistakenly put remove instead of remote:(
-
Machine: ec2-micro
-
Image: Ubuntu 22.04 LTS
-
External IP: disabled
-
Network tag: remote-client
-
remove-client-2 *I mistakenly put remove instead of remote as well :(
-
Machine: ec2-micro
-
Image: Ubuntu 24.04 LTS
-
External IP: disabled
-
Network tag: remote-client
Firewall Rule
As mentioned before, I will need to create firewall rule to access the VMs:
-
remote-server <- this firewall rule is created so I can access
remote-server
VM from mylocal machine
-
Priority: 50
-
Direction: ingress
-
Target tags: remote-server <- this attribute will bind the firewall rule to VM, through the pre-defined network tag
-
Source filter: IPv4 - My assigned ISP’s public IP. You can get this IP by simply executing curl https://ifconfig.me. in the terminal
-
Protocols to allow: all <- this is actually not a good practice as you should always only allow a specific port, but since this is a demo machine and I will dispose them, let’s keep it as it is
-
remote-client <- this firewall rule is created so
remote-server
machine can access theclients
-
Priority: 100
-
Direction: ingress
-
Target tags: remote-client
-
Source filter: IPv4 - remote-server’s IP. In this exercise, the IP is 10.0.0.2
-
Protocols to allow: all <- again, this is actually not a good practice
Accessing the Main VM
Okay! The VMs are ready. In GCP, we can utilise gcloud to access the created VM. You can follow the steps in this official documentation to install it. Once it is installed, you need to authorize it.
gcloud auth login
A browser window will be opened with a Google authorization page. You’ll need to authorize your google account that has access to the GCP account. Once it is authorized, you can access the VM directly!
gcloud compute ssh remote-server --project=faiz-rahiemy
Generate Key to Access Clients
As we have already accessed the main VM, our next step is to generate an SSH key pair to access the VM. Not having a password login method and using SSH key instead is technically more secure.
ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:xxx root@remote-server
The key's randomart image is:
+---[RSA 3072]----+
| |
| xxx |
| |
+----[SHA256]-----+
Once the key pair is ready, store the public key to the remote client VMs in GCP. On the VM edit menu, go to Security and access section, Add Item, and directly put the key there. The public key is the one that is generated from ssh-keygen
command in /root/.ssh/id_rsa.pub
. Its pair, the private key, will be used to access the VM, the default file path is /root/.ssh/id_rsa
.
Schrader - My Own Paramiko Wrapper
I’ve created a simple Python code for this exercise where it uses Paramiko as its SSH client. Before jumping further into the technicality, let’s define the requirements for our VM inventory problem:
-
The solution should be able to do all requirements remotely.
-
The solution should be able to gather hostname.
-
The solution should be able to gather OS name and version.
-
The solution should be able to do simple compliance check.
-
Define Ubuntu 24 as comply, where other OS do not comply.
The Schrader is written to answer those requirements. It is pushed in my GitHub repository: Schrader. This library contains one main file and three modules:
-
config.py - responsible for parsing flags into credentials configuration: credential source and source file (if source is file)
-
file_opener.py - a helper to open a file and get the file content into a string
-
host.py - where the Paramiko itself is located. This module is responsible for connecting to remote VM and gathering its information, and also gathering the scanner’s information as well
I wrote this library in Python 3.10
with paramiko 3.4.1
requirements.
Schrader/
├── modules/
| ├── config.py
| ├── file_opener.py
| └── host.py
├── schrader.py
├── config.txt
├── inventory.lst
└── requirements.txt
To explain further about the codes, let's break it down by the modules and their use cases.
file_opener.py
import glob
import logging
LOG = logging.getLogger('schrader')
def open_file(file_name: str) -> str | None:
LOG.debug(f'Open file: {file_name}')
results: str | None = None
try:
with open(file_name, 'r') as f:
results = f.read()
except Exception as e:
LOG.exception(f'Error: {e}')
return results
def open_file_wildcard(file_name: str) -> str:
results: str | None = ''
for filename in glob.glob(file_name):
with open(filename, 'r') as f:
results += f.read()
return results
In this file, there are two methods:
-
open_file - open file directly reading its content. The parameter for this method is the file’s path.
-
open_file_wildcard - its function is similar to open_file, but with this method, we can put a wildcard on the file’s path
config.py
import glob
import logging
import os
from modules import file_opener
LOG: logging.Logger = logging.getLogger('schrader')
LOG.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
LOG.addHandler(handler)
class Config():
username: str
password: str
key_file_name: str
def __init__(self, source: str='file', source_file: str='config.txt') -> None:
LOG.debug(f'Getting credentials from: {source}')
LOG.debug(f'Source file: {source_file}')
if source == 'file':
cred_file: str|None = file_opener.open_file(source_file)
if cred_file == None:
LOG.exception('Getting credentials file error')
else:
creds: list[str] = cred_file.split('\n')
self.username = creds[0].split('=')[1]
self.password = creds[1].split('=')[1]
self.key_file_name = creds[2].split('=')[1]
elif source == 'env':
self.username = os.getenv('SCHRADER_USERNAME') # type: ignore
self.password = os.getenv('SCHRADER_PASSWORD') # type: ignore
self.key_file_name = os.getenv('SCHRADER_KEY_FILE') # type: ignore
LOG.debug(f'username: {self.username}')
LOG.debug(f'password len: {len(self.password)}')
LOG.debug(f'key_file_name file: {self.key_file_name}')
The config file is a class where it has three variables:
-
username
-
password
-
key_file_name
Value of those variables is defined when the class is instantiated, depending on the parameters supplied. By default, it will use file as a credential source, with the default file config.txt
in the main directory. Sample content of config.txt:
username=username
password=password
key_file_name=/root/.ssh/id_rsa
Apart from using a file as a credential source, environment variable can also be used:
-
`SCHRADER_USERNAME` => fill with username
-
`SCHRADER_PASSWORD` => fill with password
-
`SCHRADER_KEY_FILE` => fill with environment
host.py
I design the host as a class thus we can instantiate and call methods depending on the use case. Let’s breakdown even further the file to answer the requirements:
-
The solution should be able to do all requirements remotely.
This requirement is where paramiko will be used. Paramiko will use credentials defined in config.py
file to establish SSH connection to the clients. This is written in connect_host
method, which will instantiate the Paramiko SSH client, add an authorized host key, and it will check whether key_file_name
is defined or not. If it is defined, it will use the private key, but if it isn’t a basic username password combination will be used. Now we can connect to the client, how can we execute the commands? It is written in run_host_command
method where it call paramiko’s exec_command
method.
def connect_host(self) -> paramiko.SSHClient|None:
LOG.debug(f'connecting to: {self.ip_address}')
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if self.config_file.key_file_name == '':
self.client.connect(hostname=self.ip_address, username=self.config_file.username, password=self.config_file.password)
else:
self.client.connect(hostname=self.ip_address, username=self.config_file.username, password=self.config_file.password, key_filename=self.config_file.key_file_name)
except Exception as e:
self.client = None # type: ignore
LOG.error(f'Connecting {self.ip_address} error')
def run_host_command(self, command: str) -> str:
LOG.debug(f'Running ssh command: {command}')
output: str = ''
if self.client != None:
try:
_, stdout, stderr = self.client.exec_command(command)
output: str = stdout.read().decode()
err: str = stderr.read().decode()
if err:
LOG.debug(f'Running ssh command error: {err}')
else:
LOG.debug(f'Command output: {output}')
except Exception as e:
LOG.error(f'Running ssh command error: {e}')
return output
-
The solution should be able to gather hostname.
This code section directly executes hostname
command to get the VM’s hostname.
hostname_file: str = self.run_host_command('hostname')
if len(hostname_file.split('\n')) > 1:
self.hostname = hostname_file.split('\n')[0]
else:
self.hostname = hostname_file
-
The solution should be able to gather OS name and version.
In this code, the library is running command to get the content of files in /etc/*release
path to gather the OS information. Then, the returned values will be parsed in get_os_information
method.
etc_release_file: str = self.run_host_command('cat /etc/*release')
self.os_name, self.os_version = self.get_os_information(etc_release_file)
...
def get_os_information(self, etc_release: str) -> tuple[str, str]:
LOG.debug('Getting os information')
etc_release_lines: list[str] = etc_release.split('\n')
os_name = 'Unknown'
os_version = 'Unknown'
temp: str = ''
for etc_release_line in etc_release_lines:
if etc_release_line != '':
temp = etc_release_line
if 'PRETTY_NAME'.lower() in etc_release_line.lower():
try:
os_name = etc_release_line.split('=')[1].replace('"', '')
except:
os_name = etc_release_line
if 'VERSION_ID'.lower() in etc_release_line.lower():
try:
os_version = etc_release_line.split('=')[1].replace('"', '')
except:
os_version = etc_release_line
if os_name == 'Unknown' and temp != '':
os_name = os_version = temp
return os_name, os_version
-
The solution should be able to do simple compliance check.
-
Define Ubuntu 24 as comply, where other OS are not comply.
This is a simple if-else code where it will check the previously gathered VM’s OS information and return True
if it is Ubuntu 24, otherwise it will return False
.
def is_os_comply(self) -> bool:
if 'ubuntu' in self.os_name.lower():
if '24' in self.os_version:
return True
return False
-
Additional
Additionally, I put codes to gather the scanner’s VM information and try to modify the remote server by creating a file in /tmp
directory.
if is_scanner:
LOG.debug('Getting own info')
try:
self.hostname: str = socket.gethostname()
except:
self.hostname = 'Unknown'
self.ip_address = '127.0.0.1'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('192.255.255.255', 1))
self.ip_address = s.getsockname()[0]
finally:
s.close()
try:
os_name, os_version = self.get_os_information(file_opener.open_file_wildcard('/etc/*release'))
except:
os_name = os_version = 'Unknown'
self.os_name = os_name
self.os_version = os_version
self.os_comply = self.is_os_comply()
...
def touch_tmp(self) -> None:
LOG.debug(f'Touching /tmp/schrader')
self.run_host_command('touch /tmp/schrader')
Complete host.py File
import logging
import paramiko
import socket
from modules import config, file_opener
LOG: logging.Logger = logging.getLogger('schrader')
LOG.setLevel(logging.DEBUG)
class Host():
config_file: config.Config
ip_address: str = ''
hostname: str = ''
os_name: str = ''
os_version: str = ''
os_comply: bool = False
client: paramiko.SSHClient|None
def __init__(self, config_file: config.Config, ip_address: str, is_scanner: bool = False):
self.config_file = config_file
self.ip_address = ip_address
if is_scanner:
LOG.debug('Getting own info')
try:
self.hostname: str = socket.gethostname()
except:
self.hostname = 'Unknown'
self.ip_address = '127.0.0.1'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('192.255.255.255', 1))
self.ip_address = s.getsockname()[0]
finally:
s.close()
try:
os_name, os_version = self.get_os_information(file_opener.open_file_wildcard('/etc/*release'))
except:
os_name = os_version = 'Unknown'
self.os_name = os_name
self.os_version = os_version
self.os_comply = self.is_os_comply()
else:
LOG.debug(f'Discovering {ip_address}')
self.connect_host()
if self.client != None:
hostname_file: str = self.run_host_command('hostname')
if len(hostname_file.split('\n')) > 1:
self.hostname = hostname_file.split('\n')[0]
else:
self.hostname = hostname_file
etc_release_file: str = self.run_host_command('cat /etc/*release')
self.os_name, self.os_version = self.get_os_information(etc_release_file)
self.os_comply = self.is_os_comply()
self.touch_tmp()
self.client.close()
LOG.debug(f'hostname: {self.hostname}')
LOG.debug(f'ip_address: {self.ip_address}')
LOG.debug(f'os_name: {self.os_name}')
LOG.debug(f'os_version: {self.os_version}')
LOG.debug(f'os_comply: {self.os_comply}')
def connect_host(self) -> paramiko.SSHClient|None:
LOG.debug(f'connecting to: {self.ip_address}')
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if self.config_file.key_file_name == '':
self.client.connect(hostname=self.ip_address, username=self.config_file.username, password=self.config_file.password)
else:
self.client.connect(hostname=self.ip_address, username=self.config_file.username, password=self.config_file.password, key_filename=self.config_file.key_file_name)
except Exception as e:
self.client = None # type: ignore
LOG.error(f'Connecting {self.ip_address} error')
def run_host_command(self, command: str) -> str:
LOG.debug(f'Running ssh command: {command}')
output: str = ''
if self.client != None:
try:
_, stdout, stderr = self.client.exec_command(command)
output: str = stdout.read().decode()
err: str = stderr.read().decode()
if err:
LOG.debug(f'Running ssh command error: {err}')
else:
LOG.debug(f'Command output: {output}')
except Exception as e:
LOG.error(f'Running ssh command error: {e}')
return output
def touch_tmp(self) -> None:
LOG.debug(f'Touching /tmp/schrader')
self.run_host_command('touch /tmp/schrader')
def get_os_information(self, etc_release: str) -> tuple[str, str]:
LOG.debug('Getting os information')
etc_release_lines: list[str] = etc_release.split('\n')
os_name = 'Unknown'
os_version = 'Unknown'
temp: str = ''
for etc_release_line in etc_release_lines:
if etc_release_line != '':
temp = etc_release_line
if 'PRETTY_NAME'.lower() in etc_release_line.lower():
try:
os_name = etc_release_line.split('=')[1].replace('"', '')
except:
os_name = etc_release_line
if 'VERSION_ID'.lower() in etc_release_line.lower():
try:
os_version = etc_release_line.split('=')[1].replace('"', '')
except:
os_version = etc_release_line
if os_name == 'Unknown' and temp != '':
os_name = os_version = temp
return os_name, os_version
def is_os_comply(self) -> bool:
if 'ubuntu' in self.os_name.lower():
if '24' in self.os_version:
return True
return False
schrader.py
This is the main file where it calls all modules from the previous sections. This main file also parses flags, instantiates config file and parses inventory file. Then, it will start discovering the hosts.
import argparse
import logging
from modules import config, host, file_opener
LOG: logging.Logger = logging.getLogger('schrader')
def main(flags: argparse.Namespace):
source: str = 'file'
source_file: str = 'config.txt'
inventory_file_name: str = 'inventory.lst'
if flags.source != None:
source = str(flags.source)
if flags.file != None:
source_file = str(flags.file)
if flags.inventory != None:
inventory_file_name = flags.inventory
config_file: config.Config = config.Config(source=source, source_file=source_file)
inventory: list[str] = parse_inventory(inventory_file_name)
discover_host(config_file, inventory)
def parse_inventory(inventory_file_name: str) -> list[str]:
inventory: list[str] = []
inventory_file: str | None = file_opener.open_file(inventory_file_name)
if inventory_file != None:
inventory = inventory_file.split('\n')
return inventory
def discover_host(config_file: config.Config, inventory: list[str]) -> list[host.Host]:
LOG.info('Gathering information')
discovered_hosts: list[host.Host] = []
discovered_hosts.append(host.Host(config_file, '127.0.0.1', True))
for host_address in inventory:
if host_address != '':
discovered_host = host.Host(config_file, host_address)
discovered_hosts.append(discovered_host)
LOG.info('Discovering hosts done. total hosts: {0}'.format(len(discovered_hosts)))
return discovered_hosts
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='Schrader',
description='Schrader is a python library to gather remote host information'
)
parser.add_argument('-s', '--source')
parser.add_argument('-f', '--file')
parser.add_argument('-i', '--inventory')
flags: argparse.Namespace = parser.parse_args()
main(flags)
Gathering VM!
Now the moment of truth, let’s gather the VMs information using Schrader!
First, ensure Python 3.10 is installed. In my GCP VM, Python 3.10.12 is installed by default. Then, download & install pip
as the Python library. We will need pip to install paramiko library later.
python3
Python 3.10.12 (main, Jul 29 2024, 16:56:48) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
curl -o get-pip.py https://bootstrap.pypa.io/get-pip.py
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2213k 100 2213k 0 0 16.3M 0 --:--:-- --:--:-- --:--:-- 16.5M
python3 get-pip.py
Collecting pip
Downloading pip-24.2-py3-none-any.whl.metadata (3.6 kB)
Collecting wheel
Downloading wheel-0.44.0-py3-none-any.whl.metadata (2.3 kB)
Downloading pip-24.2-py3-none-any.whl (1.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 46.1 MB/s eta 0:00:00
Downloading wheel-0.44.0-py3-none-any.whl (67 kB)
Installing collected packages: wheel, pip
Successfully installed pip-24.2 wheel-0.44.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable.It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
pip --version
pip 24.2 from /usr/local/lib/python3.10/dist-packages/pip (python 3.10)
pip install setuptools==69.5.1
Collecting setuptools==69.5.1
Downloading setuptools-69.5.1-py3-none-any.whl.metadata (6.2 kB)
Downloading setuptools-69.5.1-py3-none-any.whl (894 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 894.6/894.6 kB 46.6 MB/s eta 0:00:00
Installing collected packages: setuptools
Attempting uninstall: setuptools
Found existing installation: setuptools 59.6.0
Uninstalling setuptools-59.6.0:
Successfully uninstalled setuptools-59.6.0
Successfully installed setuptools-69.5.1
In the Schrader directory, I’ve put requirements.txt
file containing a single dependency: paramiko==3.4.1. Install the dependency.
cd schrader/
pip install -r requirements.txt
Collecting paramiko==3.4.1 (from -r requirements.txt (line 1))
Using cached paramiko-3.4.1-py3-none-any.whl.metadata (4.4 kB)
Requirement already satisfied: bcrypt>=3.2 in /usr/lib/python3/dist-packages (from paramiko==3.4.1->-r requirements.txt (line 1)) (3.2.0)
Requirement already satisfied: cryptography>=3.3 in /usr/lib/python3/dist-packages (from paramiko==3.4.1->-r requirements.txt (line 1)) (3.4.8)
Collecting pynacl>=1.5 (from paramiko==3.4.1->-r requirements.txt (line 1))
Using cached PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl.metadata (8.6 kB)
Collecting cffi>=1.4.1 (from pynacl>=1.5->paramiko==3.4.1->-r requirements.txt (line 1))
Using cached cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Collecting pycparser (from cffi>=1.4.1->pynacl>=1.5->paramiko==3.4.1->-r requirements.txt (line 1))
Using cached pycparser-2.22-py3-none-any.whl.metadata (943 bytes)
Using cached paramiko-3.4.1-py3-none-any.whl (226 kB)
Using cached PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (856 kB)
Using cached cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (445 kB)
Using cached pycparser-2.22-py3-none-any.whl (117 kB)
Installing collected packages: pycparser, cffi, pynacl, paramiko
Successfully installed cffi-1.17.0 paramiko-3.4.1 pycparser-2.22 pynacl-1.5.0
Now let’s set the configuration file that contains the credentials to access the client:
vi config.txt
username=root
password=password
key_file_name=/root/.ssh/id_rsa
The last requirement is putting the client list as inventory
vi inventory.lst
10.0.0.3
10.0.0.4
All pre-requisites are done, now let’s run the schreder!
python3 schrader.py -s file -f config.txt -i inventory.lst
2024-08-24 13:22:00,699 - schrader - DEBUG - Getting credentials from: file
2024-08-24 13:22:00,699 - schrader - DEBUG - Source file: config.txt
2024-08-24 13:22:00,699 - schrader - DEBUG - Open file: config.txt
2024-08-24 13:22:00,700 - schrader - DEBUG - username: root
2024-08-24 13:22:00,700 - schrader - DEBUG - password len: 8
2024-08-24 13:22:00,700 - schrader - DEBUG - key_file_name file: /root/.ssh/id_rsa
2024-08-24 13:22:00,700 - schrader - DEBUG - Open file: inventory.lst
2024-08-24 13:22:00,700 - schrader - INFO - Gathering information
2024-08-24 13:22:00,700 - schrader - DEBUG - Getting own info
2024-08-24 13:22:00,701 - schrader - DEBUG - Getting os information
2024-08-24 13:22:00,701 - schrader - DEBUG - hostname: remote-server
2024-08-24 13:22:00,701 - schrader - DEBUG - ip_address: 10.0.0.2
2024-08-24 13:22:00,701 - schrader - DEBUG - os_name: Ubuntu 22.04.4 LTS
2024-08-24 13:22:00,701 - schrader - DEBUG - os_version: 22.04
2024-08-24 13:22:00,701 - schrader - DEBUG - os_comply: False
2024-08-24 13:22:00,701 - schrader - DEBUG - Discovering 10.0.0.3
2024-08-24 13:22:00,701 - schrader - DEBUG - connecting to: 10.0.0.3
2024-08-24 13:22:00,891 - schrader - DEBUG - Running ssh command: hostname
2024-08-24 13:22:01,598 - schrader - DEBUG - Command output: remove-client-1
2024-08-24 13:22:01,598 - schrader - DEBUG - Running ssh command: cat /etc/*release
2024-08-24 13:22:01,650 - schrader - DEBUG - Command output: DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.4 LTS"
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
2024-08-24 13:22:01,651 - schrader - DEBUG - Getting os information
2024-08-24 13:22:01,651 - schrader - DEBUG - Touching /tmp/schrader
2024-08-24 13:22:01,651 - schrader - DEBUG - Running ssh command: touch /tmp/schrader
2024-08-24 13:22:01,702 - schrader - DEBUG - Command output:
2024-08-24 13:22:01,702 - schrader - DEBUG - hostname: remove-client-1
2024-08-24 13:22:01,703 - schrader - DEBUG - ip_address: 10.0.0.3
2024-08-24 13:22:01,703 - schrader - DEBUG - os_name: Ubuntu 22.04.4 LTS
2024-08-24 13:22:01,703 - schrader - DEBUG - os_version: 22.04
2024-08-24 13:22:01,703 - schrader - DEBUG - os_comply: False
2024-08-24 13:22:01,703 - schrader - DEBUG - Discovering 10.0.0.4
2024-08-24 13:22:01,703 - schrader - DEBUG - connecting to: 10.0.0.4
2024-08-24 13:22:01,874 - schrader - DEBUG - Running ssh command: hostname
2024-08-24 13:22:02,689 - schrader - DEBUG - Command output: remove-client-2.asia-southeast1-c.c.faiz-rahiemy.internal
2024-08-24 13:22:02,690 - schrader - DEBUG - Running ssh command: cat /etc/*release
2024-08-24 13:22:02,738 - schrader - DEBUG - Command output: DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04 LTS"
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
2024-08-24 13:22:02,739 - schrader - DEBUG - Getting os information
2024-08-24 13:22:02,739 - schrader - DEBUG - Touching /tmp/schrader
2024-08-24 13:22:02,739 - schrader - DEBUG - Running ssh command: touch /tmp/schrader
2024-08-24 13:22:02,787 - schrader - DEBUG - Command output:
2024-08-24 13:22:02,788 - schrader - DEBUG - hostname: remove-client-2.asia-southeast1-c.c.faiz-rahiemy.internal
2024-08-24 13:22:02,788 - schrader - DEBUG - ip_address: 10.0.0.4
2024-08-24 13:22:02,788 - schrader - DEBUG - os_name: Ubuntu 24.04 LTS
2024-08-24 13:22:02,788 - schrader - DEBUG - os_version: 24.04
2024-08-24 13:22:02,788 - schrader - DEBUG - os_comply: True
2024-08-24 13:22:02,788 - schrader - INFO - Discovering hosts done. total hosts: 3
The code works like a charm! All two clients plus the main VM’s information are gather in single file execution. Furthermore, it also managed to check whether the VM use comply OS or not, where only remove-client-2 that comply since it use Ubuntu 24.04 LTS.
Summary
-
Paramiko can answer all defined requirements to gather VM information remotely.
-
Can write files as well, basically can do anything by running the exec_command method.
-
Need Python scripting skills to develop the wrapper. No need to be an expert, but writing the script takes quite some time.
-
Need a uniform credential for all clients for basic code.
-
Need SSH access from scanner VM to client VMs.
-
Highly customizable.
-
While not demonstrated in this exercise, Paramiko supports SCP to transfer files as well.
Now we have exercised using Paramiko, I’m thinking of several other tools to explore to solve similar requirements, should it be Ansible, Chef, or should I explore an agent-based solution?
Give Comments
* required fields
Comments
Be the first to comment!