~/blog
~/blog$ render faiz.blog.building-vm-inventory-#1:-paramiko

Building VM Inventory #1: Paramiko

Sunday, 25 August 2024 01:13:44 WIB | tags: tips, python, gcp, vm-inventory | 219 hits | 0 comment(s)

Building VM Inventory #1: Paramiko

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

Preparing GCP Environment

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 my local 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 the clients

    • 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?

Comments

Be the first to comment!

Give Comments









* required fields

Sending comment...

~/blog$ shortcuts: > Notes and > Faiz?