Python 100 project #51: Web scraping – Sunshine duration across countries

It’s said to be London is always covered with cloud. As I moved to London roughly two years ago, I realized it is actually not the case.

I searched the web and found very useful wikipedia page to list the (typically average) sunshine duration among each month of a year. This is a very basic task for web scraping (just 1 page).

 

Output:

 

 

Code:

# _*_ coding: utf-8 _*_

import csv
import re
from urllib.parse import urljoin

from bs4 import BeautifulSoup
import requests

base = "https://en.wikipedia.org"
target_url = base + "/wiki/List_of_cities_by_sunshine_duration"
req = requests.get(target_url, verify=False)

bs = BeautifulSoup(req.text, "html.parser")

tables = bs.find_all("table", {"class": "wikitable"})

cities_list = []

for table in tables:
    cities = table.find_all("tr")
    for city in cities:
        city_row = []
        # for text data collection. country_name, country_url, city_name, city_url
        for text_elem in city.find_all("td", style=re.compile("text-align:left")):
            elem_text = text_elem.get_text()
            city_row.append(elem_text)
            if text_elem.find("a"):
                city_row.append(urljoin(base, text_elem.find("a").get("href")))
            else:
                city_row.append("")
        # for sunshine hours data in monthly sequence.
        for data_elem in city.find_all("td", style=re.compile("background.*")):
            elem_text = data_elem.get_text()
            city_row.append(elem_text)

        cities_list.append(city_row)

with open('sunshine_hours.csv', 'w', newline='') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerows(cities_list)

 

Python 100 project #50: Get Audit Report on Slack

In this project, I extended the previous project “PDF to TXT”, and now it’s posted to Slack every day.

So in short, every day the sophos XG firewall sends the security audit report(PDF) to the python powered server, and the server interpret the PDF into the text, (and of course it selects the necessary part only) and post the daily summary on slack.

 

Output:

 

 

 

Code:

import base64
from io import BytesIO
from pprint import pprint
import tempfile

import aiosmtpd.controller
import asyncio
import email

import audit_reader
import slack


class CustomSMTPHandler:
    async def handle_DATA(self, server, session, envelope):

        msg = email.message_from_string(str(envelope.content,'utf-8'))

        for part in msg.walk():
            if part.get_content_type().startswith("application/pdf"):
                pdf_bytes = BytesIO(part.get_payload(decode=True))
                data = audit_reader.retrieve_data(pdf_bytes)
                slack.post(data, 'security_logs', envelope.mail_from)
        print('from:', envelope.mail_from)
        return '250 OK'


async def main(loop):
    handler = CustomSMTPHandler()
    server = aiosmtpd.controller.Controller(handler,hostname='XX.XX.XX.XX', port=XXXX)
    server.start()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.create_task(main(loop=loop))
    try:
        print("server running...")
        loop.run_forever()
    except KeyboardInterrupt:
        pass

 

Python 100 project #49: PDF to Text Converter

There are several file format which looks user friendly but it is difficult to digest in data process. One of them is PDF. It has lots of contents inside, hence it’s usually very tough to get information out of it programatically.

This time, I used Sophos XG Firewall Daily Executive Report PDF to retrieve Hardware info.

 

Output:

$ python soreader.py 
{"CPU Usage": [{"idle_average": 93.98}, {"idle_min": 28.36}]}
{"Memory Usage": [{"used_min": 1.53}, {"used_average": 1.59}, {"used_max": 1.66}, {"total": "1.95"}]}

 

Code:

from itertools import izip_longest
import json
import re

import pdftotext

PARAMS = [
    "CPU Usage", "Memory Usage", "Disk Usage"
    ]


def retrieve_data(pdf):
    result_dict = {}
    # Iterate over all the pages
    for page in pdf:
        first_sentence = re.match(r"(.*?)\n", page)
        if first_sentence:
            param = first_sentence.group(1)
            if param in PARAMS:
                matched_obj = re.search(r"\n((.*?)\n){3,4}", page)
                matched_txt = matched_obj.group(0)
                data_list = [ cell.split() for cell in matched_txt.split("\n") ]
                result_dict[param] = data_list
    
    return result_dict


def cpu_usage_idle_parser(table):
    # this function just return idle parameter(MIN, AVERAGE) only.
    for row in table:
        if 'Idle' in row:
            raw_data_list = row[row.index('Idle')+1:]
            data_list = list(map(lambda x: float(x[:-1]), raw_data_list))
            # remove largest data, which should be the MAX value
            data_list.remove(max(data_list))
            idle_average = max(data_list)
            idle_min = min(data_list)
            
            result = {'CPU Usage': [
                {'idle_average': idle_average},
                {'idle_min': idle_min},
                ]}
                
            return json.dumps(result)
    return None


def memory_usage_parser(table):
    # this function returns memory utilization(used: MIN, AVERAGE, MAX).
    for row in table:
        if 'Used' in row:
            data_list = []
            for column in row:
                try:
                    formated_data = float(column)
                    data_list.append(formated_data)
                except ValueError:
                    pass
            used_min, used_avg, used_max = sorted(data_list)
        elif 'Total' in row:
            total_memory = row[row.index('Total')+1]
        
    result = {'Memory Usage': [
        {'used_min': used_min},
        {'used_average': used_avg},
        {'used_max': used_max},
        {'total': total_memory},
        ]}
            
    return json.dumps(result)


if __name__ == "__main__":
    # Load PDF file
    with open("testreport.pdf", "rb") as f:
        pdf = pdftotext.PDF(f)

    data = retrieve_data(pdf)
    
    print(cpu_usage_idle_parser(data['CPU Usage']))
    
    print(memory_usage_parser(data['Memory Usage']))

 

Python 100 project #42: Slack Bot – AWS EC2 list

Following up the previous project, I created Slack bot to get EC2 instance list (of all regions) in one shot.

So now no need to open the terminal to invoke the command every time. Just need to ask Slack “/100p ec2 list” and the result is posted.

I used AWS API Gateway to receive the slash command from Slack. So it is easy to add functions.

 

Output Example:

 

Here is the code:

This is the receiver code which is invoked when Slack slach command post request to API Gateway.

from base64 import b64decode
import json
import os
from urllib.parse import parse_qs
import logging

import boto3


ENCRYPTED_EXPECTED_TOKEN = "kms_base64encodedkey="

kms = boto3.client('kms')
expected_token = str(kms.decrypt(CiphertextBlob = b64decode(ENCRYPTED_EXPECTED_TOKEN))['Plaintext'], 'utf-8')


logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    req_body = event['body']
    params = parse_qs(req_body)
    print("received data...", params)
    token = params['token'][0]
    if token != expected_token:
        logger.error("Request token (%s) does not match exptected", token)
        raise Exception("Invalid request token")

    user = params['user_name'][0]
    command = params['command'][0]
    channel = params['channel_name'][0]
    if 'text' in params.keys():
        command_text = params['text'][0]
    else:
        command_text = ''
    response_url = params['response_url'][0]
    arg = command_text.split(' ')
    
    sns = boto3.client('sns')
    SNS_CHANNEL = os.environ['SNS_CHANNEL']
    topic_arn = sns.create_topic(Name=SNS_CHANNEL)['TopicArn']
    message={"user_name": user, "command": command, "channel": channel, "command_text": command_text, "response_url": response_url}
    message=json.dumps(message)
    message=json.dumps({'default': message, 'lambda': message})
    response = sns.publish(
        TopicArn=topic_arn,
        Subject='/100p',
        MessageStructure='json',
        Message=message
    )
    return { "text": "received command - %s . Please wait for a few seconds for the reply to be posted." % (command_text) }

 

And this is the actual code to post the result to the Slack.

import json
import sys

import boto3
import requests

def get_regions(service):
    credential = boto3.session.Session()
    return credential.get_available_regions(service)


def list_ec2_servers(region):
    credential = boto3.session.Session()
    ec2 = credential.client('ec2', region_name=region)
    instances = ec2.describe_instances()
    servers_list = []
    for reservations in instances['Reservations']:
        for instance in reservations['Instances']:
            tags = parse_keyvalue_sets(instance['Tags'])
            state = instance['State']['Name']
            servers_list.append([region, instance['InstanceId'], tags['Name'], state])
    return servers_list


def parse_keyvalue_sets(tags):
    result = {}
    for tag in tags:
        key = tag['Key']
        val = tag['Value']
        result[key] = val
    return result


def lambda_handler(event, context):
    message = event['Records'][0]['Sns']['Message']
    try:
        message = json.loads(message)
        user_name = message['user_name']
        command = message['command']
        command_text = message['command_text']
        response_url = message['response_url']
        arg = command_text.split(' ')

        if arg[0] == 'ec2':
            resp = ec2_helper(arg[1:])
        # TODO else: statement for other functions

        # if response_type is not specified, act as the same as ephemeral
        # ephemeral, response message will be visible only to the user
        slack_message = {
            'channel': '@%s' % user_name,
            # 'response_type': 'in_channel',
            'response_type': 'ephemeral',
            'isDelayedResponse': 'true',
            'text': resp
        }
        print("Send message to %s %s" % (response_url, slack_message))
        header = {'Content-Type': 'application/json'}
        response = requests.post(response_url, headers=header, data=json.dumps(slack_message))
        if response.status_code == 200:
            print("Message posted to %s" % slack_message['channel'])
    except requests.exceptions.RequestException as e:
        print(e)
    except:
        e = sys.exc_info()[0]
        print("Something wrong happened...", e)

def ec2_helper(command):
    regions = get_regions('ec2')

    if command[0] == 'list':
        region_servers = []
        for region in regions:
            servers = list_ec2_servers(region)
            region_servers.extend(servers)

        msg = ""
        for server in region_servers:
            msg += '\t'.join(server)
            msg += "\n"
    # TODO else for other functions

    return msg

 

Python 100 project #42: AWS Data Post to Slack – Billing

Sometimes, I forgot to stop the AWS instance, and I’m lazy to check the running instance for a while. Then it suddenly comes clear when I receives the email from AWS for the billing of the previous month.

To avoid this surprise, I created Lambda function to post the estimated cost of the period every morning to the slack.

 

Output Example:

 

Here is the (main handler) code:

import datetime
import logging
import os

import boto3
import requests

import slack

SLACK_CHANNEL = os.environ['SLACK_CHANNEL']

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def estimated_cost():
    response = boto3.client('cloudwatch', region_name='us-east-1')

    get_metric_statistics = response.get_metric_statistics(
        Namespace='AWS/Billing',
        MetricName='EstimatedCharges',
        Dimensions=[
            {
                'Name': 'Currency',
                'Value': 'USD'
            }
        ],
        StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
        EndTime=datetime.datetime.today(),
        Period=86400,
        Statistics=['Maximum']
    )
    
    return get_metric_statistics['Datapoints'][0]['Maximum']


def lambda_handler(event, context):
    date = get_metric_statistics['Datapoints'][0]['Timestamp'].strftime('%Y-%m-%d')
    cost = estimated_cost()
    content = "Estimated cost is %s as of %s" % (cost, date)

    try:
        slack.post(content, SLACK_CHANNEL, context.function_name)
        logger.info("Message posted to %s, %s" % (SLACK_CHANNEL, content))
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)

 

Python 100 project #41: Syslog Post to Slack – Content Filtering

Following up the last project, I created another function so that my syslog server can post slack upon rejection of client request due to the content filtering.

 

Output Example:

 

Here is the new modified syslog_server.py with new function and some rearrangement of the function:

## Reference https://gist.github.com/marcelom/4218010

## Tiny Syslog Server in Python.
##
## This is a tiny syslog server that is able to receive UDP based syslog
## entries on a specified port and save them to a file.
## That's it... it does nothing else...
## There are a few configuration parameters.

HOST, PORT = "0.0.0.0", 514
PRINT_LOG = True

# SYSLOG Notification parameter
CONTENT_FILTERING_NOTIFY = True

#
# NO USER SERVICEABLE PARTS BELOW HERE...
#

import logging
import re
import socketserver
import sys

import custom_helper.slack


class SyslogUDPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        data = bytes.decode(self.request[0].strip(), encoding="utf-8")
        socket = self.request[1]
        if PRINT_LOG:
            print("%s : " % self.client_address[0], str(data.encode("utf-8")))
        if CONTENT_FILTERING_NOTIFY:
            cf_notify(data)
        logging.info(str(data.encode("utf-8")))


def cf_notify(log):
    log_match = re.search(r'log_type="(.*?)".*log_subtype="(.*?)".*category="(.*?)".*url="(.*?)"', log)
    if log_match[1] == "Content Filtering" and log_match[2] == "Denied":
        category, url = log_match[3], log_match[4]
        custom_helper.slack.post(f"Content Filtering Denied: {category} - {url}", "security_logs", "HOME_SOPHOS")


if __name__ == "__main__":

    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} log_file_name")
        sys.exit(0)

    try:
        LOG_FILE = sys.argv[1]
        logging.basicConfig(level=logging.INFO, format='%(message)s', datefmt='', filename=LOG_FILE, filemode='a')
        server = socketserver.UDPServer((HOST,PORT), SyslogUDPHandler)
        server.serve_forever(poll_interval=0.5)
    except (IOError, SystemExit):
        raise
    except KeyboardInterrupt:
        print ("Crtl+C Pressed. Shutting down.")

 

Python 100 project #40: Syslog Server

I’m using Sophos XG Firewall VM at home. It is fantastic in terms of the feature and UI, it really works well and suits my needs for daily web surfing (and its protection). But it lacks some enterprise features. One of the measure feature I need these kind of device is alert customization. It should be able to notify the admin if any changes(or event) occurs.

At this moment, it is in the vote list, but there is no plan this function to be supported. Hence I decided to use syslog to get customized alert in real time. As a first step, I searched python3 powered syslog server, and modified a bit.

 

Here is the syslog server output:

# python3.6 syslog_server.py testlog.log
192.168.1.180 :  b'<134>device="SFW" date=2018-06-11 time=00:20:46 timezone="BST" device_name="SFVH" device_id=C01001QMP929K6A log_id=050902616002 
log_type="Content Filtering" log_component="HTTP" log_subtype="Denied" status="" priority=Information fw_rule_id=2 user_name="" user_gp="" 
iap=12 category="Gambling" category_type="Objectionable" url="https://www.magicalvegas.com/" contenttype="" override_token="" httpresponsecode="" 
src_ip=10.10.10.2 dst_ip=212.30.13.135 protocol="TCP" src_port=62469 dst_port=443 sent_bytes=0 recv_bytes=0 domain=www.magicalvegas.com exceptions= 
activityname="Not Suitable for Schools" reason="" user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" 
status_code="403" transactionid= referer="https://www.google.co.uk/"'

 

Here is the code:

## Reference https://gist.github.com/marcelom/4218010

## Tiny Syslog Server in Python.
##
## This is a tiny syslog server that is able to receive UDP based syslog
## entries on a specified port and save them to a file.
## That's it... it does nothing else...
## There are a few configuration parameters.

# LOG_FILE = 'youlogfile.log'
HOST, PORT = "0.0.0.0", 514

#
# NO USER SERVICEABLE PARTS BELOW HERE...
#

import logging
import socketserver
import sys


class SyslogUDPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        data = bytes.decode(self.request[0].strip(), encoding="utf-8")
        socket = self.request[1]
        print("%s : " % self.client_address[0], str(data.encode("utf-8")))
        logging.info(str(data.encode("utf-8")))


if __name__ == "__main__":

    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} log_file_name")
        sys.exit(0)

    try:
        LOG_FILE = sys.argv[1]
        logging.basicConfig(level=logging.INFO, format='%(message)s', datefmt='', filename=LOG_FILE, filemode='a')
        server = socketserver.UDPServer((HOST,PORT), SyslogUDPHandler)
        server.serve_forever(poll_interval=0.5)
    except (IOError, SystemExit):
        raise
    except KeyboardInterrupt:
        print ("Crtl+C Pressed. Shutting down.")

 

Python 100 project #39: Email Inbound-SMTP Test

When introducing a new security layer to the email service, sometimes I need to test if those servers are working well without changing anything in production server. It is usually quite difficult as Email services are usually company wide. But if I can specify the smtp server to use(in this case the new security appliance server ip address), it’s possible at least I can see if that server is configured correctly(to receive email).

 

Output Example:

$ python3 mail_helper.py 
This utility sends the email using specified SMTP server.
Please enter smtp server: 108.177.15.26

Please enter from address: admin@example.com

Please enter to address: XXXXXXXX@gmail.com

Please enter the subject of this email: this is a test subject

Please enter mail body: this is a test body from python3

Please enter file name (in full path) if any separated by space: /Users/XXXXX/test.csv /Users/XXXXXX/test.zip

Email has been sent.

 

 

Here is the code:

import smtplib
from email import encoders, utils
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import mimetypes


def attachment(filename):
    mimetype, mimeencoding = mimetypes.guess_type(filename)
    if mimeencoding or (mimetype is None):
        mimetype = 'application/octet-stream'
    maintype, subtype = mimetype.split('/')
    if maintype == 'text':
        with open(filename, 'r') as fd:
            retval = MIMEText(fd.read(), _subtype=subtype)
    else:
        with open(filename, 'rb') as fd:
            retval = MIMEBase(maintype, subtype)
            retval.set_payload(fd.read())
            encoders.encode_base64(retval)
    retval.add_header('Content-Disposition', 'attachment', filename=filename)
    return retval


def create_message(fromaddr, toaddr, subject, message, files):
    msg = MIMEMultipart()
    msg['To'] = toaddr
    msg['From'] = fromaddr
    msg['Subject'] = subject
    msg['Date'] = utils.formatdate(localtime=True)
    msg['Message-ID'] = utils.make_msgid()

    body = MIMEText(message, _subtype='plain')
    msg.attach(body)

    for filename in files:
        msg.attach(attachment(filename))
    return msg.as_string()


def send(smtpsrv, fromaddr, toaddr, message):
    s = smtplib.SMTP(host=smtpsrv, port=25)
    s.sendmail(fromaddr, [toaddr], message)
    s.close()


if __name__ == '__main__':

    print("This utility sends the email using specified SMTP server.")
    smtpsrv = input("Please enter smtp server: ")
    print()
    fromaddr = input("Please enter from address: ")
    print()
    toaddr = input("Please enter to address: ")
    print()
    subject = input("Please enter the subject of this email: ")
    print()
    msg = input("Please enter mail body: ")
    print()
    attach = input("Please enter file name (in full path) if any separated by space: ").split()
    print()
    message = create_message(fromaddr, toaddr, subject, msg, attach)
    try:
        send(smtpsrv, fromaddr, toaddr, message)
        print("Email has been sent.")
    except:
        print("Something wrong...")

 

“Python 100 project #38: Windows Network Troubleshoot

It’s never been easy to troublshoot end users PC problems. For me, I’m working in SI, and our main target is SMB. Most of them are not tech, but rather than just a user. So it’s often very difficult to extract the basic information from them.

I created simple information gathering tool for windows just to collect few network related information.

 

Output Example:

On 233508=====================
route print===================================================================================================
Interface List
  3...12 50 04 b4 6a 10 ......AWS PV Network Device #0
  1...........................Software Loopback Interface 1
  6...00 00 00 00 00 00 00 e0 Microsoft ISATAP Adapter
  7...00 00 00 00 00 00 00 e0 Teredo Tunneling Pseudo-Interface
===========================================================================

IPv4 Route Table
===========================================================================
Active Routes:
Network Destination        Netmask          Gateway       Interface  Metric
          0.0.0.0          0.0.0.0       172.31.1.1     172.31.1.232     25
        127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
        127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
  127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
  169.254.169.250  255.255.255.255       172.31.1.1     172.31.1.232     50
  169.254.169.251  255.255.255.255       172.31.1.1     172.31.1.232     50
  169.254.169.254  255.255.255.255       172.31.1.1     172.31.1.232     50
       172.31.1.0    255.255.255.0         On-link      172.31.1.232    281
     172.31.1.232  255.255.255.255         On-link      172.31.1.232    281
     172.31.1.255  255.255.255.255         On-link      172.31.1.232    281
        224.0.0.0        240.0.0.0         On-link         127.0.0.1    331
        224.0.0.0        240.0.0.0         On-link      172.31.1.232    281
  255.255.255.255  255.255.255.255         On-link         127.0.0.1    331
  255.255.255.255  255.255.255.255         On-link      172.31.1.232    281
===========================================================================
Persistent Routes:
  Network Address          Netmask  Gateway Address  Metric
  169.254.169.254  255.255.255.255       172.31.1.1      25
  169.254.169.250  255.255.255.255       172.31.1.1      25
  169.254.169.251  255.255.255.255       172.31.1.1      25
===========================================================================

IPv6 Route Table
===========================================================================
Active Routes:
 If Metric Network Destination      Gateway
  7    331 ::/0                     On-link
  1    331 ::1/128                  On-link
  7    331 2001::/32                On-link
  7    331 2001:0:4137:9e76:428:53b:53e0:fe17/128
                                    On-link
  3    281 fe80::/64                On-link
  7    331 fe80::/64                On-link
  7    331 fe80::428:53b:53e0:fe17/128
                                    On-link
  3    281 fe80::39be:13ca:2a9b:a07/128
                                    On-link
  1    331 ff00::/8                 On-link
  3    281 ff00::/8                 On-link
  7    331 ff00::/8                 On-link
===========================================================================
Persistent Routes:
  None
END===============================

netsh winhttp show proxy========================
Current WinHTTP proxy settings:

    Direct access (no proxy server).

END===============================

ping 8.8.8.8========================
Pinging 8.8.8.8 with 32 bytes of data:
Reply from 8.8.8.8: bytes=32 time<1ms TTL=51
Reply from 8.8.8.8: bytes=32 time<1ms TTL=51
Reply from 8.8.8.8: bytes=32 time<1ms TTL=51
Reply from 8.8.8.8: bytes=32 time<1ms TTL=51

Ping statistics for 8.8.8.8:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms
END===============================

tracert -d 8.8.8.8========================
Tracing route to 8.8.8.8 over a maximum of 30 hops

  1     *        *        *     Request timed out.
  2     *        *        *     Request timed out.
  3     *        *        *     Request timed out.
  4     *        *        *     Request timed out.
  5     *        *        *     Request timed out.
  6    <1 ms    <1 ms    <1 ms  100.65.8.97 
  7    16 ms     2 ms     2 ms  72.21.220.46 
  8    <1 ms    <1 ms    <1 ms  52.93.27.226 
  9    13 ms    18 ms    13 ms  52.93.26.53 
 10    <1 ms    <1 ms    <1 ms  52.93.27.156 
 11    <1 ms    <1 ms    <1 ms  52.95.219.141 
 12    <1 ms    <1 ms    <1 ms  108.170.246.65 
 13     1 ms     1 ms    <1 ms  209.85.255.45 
 14    <1 ms    <1 ms    <1 ms  8.8.8.8 

Trace complete.
END===============================

External_IP========================
18.205.162.XXX
END===============================

 

Here is the code:

from datetime import datetime
from os import popen


def cmd_execute(command):
    print(f"Executing '{command}'...")

    return_vals = popen(command).read()

    print(return_vals)

    if return_vals:
        return return_vals
    else:
        return None


def get_external_ip():
    print(f"Executing '{command}'...")
    from requests import get

    resp = get('https://api.ipify.org')
    if resp.status_code == 200:
        return resp.text
    else:
        return None

if __name__ == "__main__":

    commands = ["route print",
                "netsh winhttp show proxy",
                "ping 8.8.8.8",
                "tracert -d 8.8.8.8",
                ]

    filename = "tspackage_" + datetime.today().strftime('%Y%m%d') + ".log"

    with open(filename, "a") as f:
        f.write(f"\nOn {datetime.today().strftime('%H%M%S')}=====================")

    for command in commands:
        ret = cmd_execute(command)
        if ret:
            with open(filename, "a") as f:

                f.write(f"\n{command}========================")
                f.write(ret)
                f.write("END===============================\n")

    external_ip = get_external_ip()
    if external_ip:
        with open(filename, "a") as f:
            f.write(f"\nExternal_IP========================")
            f.write(external_ip)
            f.write("END===============================\n")