반응형

 

 

작성일: 2024년 3월 27일

 

 

업무 때문에 이틀 정도 Python 코드를 작성할 일이 생겼는데, On-line tutorial을 보고 너무 잘 만들어져 있어서 깜짝 놀랐다.

Python 책을 따로 구입하지 않아도 될 정도로 공식 Tutorial 문서만으로 스터디가 가능하다.

 

Python 학습에 도움이 되는 문서

Python 자습서 (Tutorial)

Python에 관한 모든 문서(공식 문서)

  위 문서에 담겨있는 내용

  • Library reference
  • Language reference
  • Python setup, usage
  • Howto
  • Python Module 설치
  • C/C++ 언어를 이용한 구현 확장(Extending and embedding)
  • Python's C API
  • FAQ

 

 

 


 

반응형

 

작성일: 2024월 3월 26일

 

내 밥벌이가 Python 언어로 개발하는 일이 아니다보니, Python으로 뭔가 만드는 일이 1년에 한 번 있을까 말까한다.
(누가 시켜서 만든다기 보다는 일하다가 자동화가 필요한 상황이 생길 때 ~~~)

오늘도 Python Logging 기능을 5년 만에 쓸 일이 있어서 다시 작성하려고 보니,
구글링해야 하고.. 예제 코드 작성해서 확인하고, 그런 후에 작성하려고 한 application에 적용하고...
이렇게 시간을 보내고 나니까 나 스스로 참 답답한 느낌이다.
다음 5년 뒤에도 이런 답답함이 없기를 바라면서 오늘 작성한 것을 잘 메모해야 겠다 ^^

 

 

#!/usr/bin/python3

import logging
import logging.handlers
import os
import datetime
import threading
import time


class MyLog():
    def __init__(self, dir, logname) -> None:
        self.logname = logname
        self.dir = os.path.join(dir,logname)
        self.InitLogger()

    def InitLogger(self):
        ## Log format 설정하기
        formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(filename)s:%(lineno)d - %(message)s")
        if os.path.exists(self.dir) == False :
            os.makedirs(self.dir)

        log_File = os.path.join(self.dir,  self.logname + ".log")
        timedfilehandler = logging.handlers.TimedRotatingFileHandler(filename=log_File, when='midnight', interval=1, encoding='utf-8')
        timedfilehandler.setFormatter(formatter)
        timedfilehandler.suffix = "%Y%m%d.log"

        self.logger = logging.getLogger(self.logname)
        self.logger.addHandler(timedfilehandler)
        self.logger.setLevel(logging.INFO)

        self.delete_old_log(self.dir, 3)

        now = datetime.datetime.now()
        self.toDay = "%04d-%02d-%02d" % (now.year, now.month, now.day)
        self.th_auto_delete = threading.Thread(target=self.auto_delete, daemon=True)
        self.th_auto_delete.start()

    '''
    함수 인자 설명:
      - fpath:삭제할 파일이 있는 디렉토리,
      - age:경과일수
    '''
    def delete_old_log(self, fpath, age):
        for fp in os.listdir(fpath):
            fp = os.path.join(fpath, fp)
            if os.path.isfile(fp):
                timestamp_now = datetime.datetime.now().timestamp()
                if os.stat(fp).st_mtime < timestamp_now - (age * 24 * 60 * 60):
                    try:
                        os.remove(fp)
                    except OSError:
                        print(fp, 'this log file is not deleted')

    def auto_delete(self):
        while True:
            now = datetime.datetime.now()
            day = "%04d-%02d-%02d" % (now.year, now.month, now.day)
            if self.toDay != day:
                self.toDay = day
                self.delete_old_log(self.dir, 3)
            time.sleep(600)


## This is test code.
if __name__ == '__main__':
    log = MyLog("my_log_test_dir", logname="sejong")
    for idx in range(3):
        log.logger.info(f"로그 메시지가 잘 기록되는지 테스트 중 {idx}")

 

 

테스트는 아래와 같이 하면 된다. 일단, 잘 동작함, OK !!

$ ./my_log.py
...

 

 

위 'MyLog' 클래스를 다른 Application code에서 참고한다면, 아래 예제와 같이 작성하면 된다.

참고: my_log.py는 아래 소스 코드 my_sample_app.py와 같은 디렉토리에 있어야 한다.
#!/usr/bin/python3

import my_log

...
... 중간 생략 ...
...

if __name__ == '__main__':
    log = my_log.MyLog("log_my_sample_dir", logname="my_sample")
    log.logger.info("#####  Start program  #####")
    
... 중간 생략 ...

 

$ ./my_sample_app.py
...

 


 

반응형

 

작성일: 2024년 3월 25일

 

 

Bind9 Zone 파일의 Serial 값을 변경하는 Python Script

지금 급하게 DNS 서버(Bind9)의 Serial 값을 자동 갱신하도록하는 기능이 필요해서 작성해본 Python script이다.

동작 방식을 간단하게 설명하면 이렇다.

 

만약 오늘이 "2024년 03월 25일"이라고 가정하면,

  • 기존 Zone 파일의 Serial 값이 2024032501 이면, 2024032502 으로 끝자리면 1 증분시켜줌.
  • 기존 Zone 파일의 Serial 값이 2024032401 이면, 2024032500 으로 날짜를 오늘로 변경하고 끝 2자리는 00으로 설정

아래 Script를 복사해서 바로 실행하면 잘 동작할 것이다.

$ cat update_zone_serial.py


#!/usr/bin/python3

from datetime import datetime
import shutil


##
## File 내용 중에서 일부 문자열을 찾아서 바꿈.
##
def replace_in_file(file_path, old_str, new_str):
    # 파일 읽어들이기
    fr = open(file_path, 'r')
    lines = fr.readlines()
    fr.close()

    # old_str -> new_str 치환
    fw = open(file_path, 'w')
    for line in lines:
        fw.write(line.replace(old_str, new_str))
    fw.close()


##
## Zone file에 있는 Serial 값을 찾기
## NOTE: 주의할 점
##   Zone file 안에 Serial 값이 아래 포맷으로 저장되어 있어야 한다.
##
##   @ IN SOA mydomain.kr root.mydomain.kr (
##     2024032502    ; Serial
##     3600          ; Refresh
##     900           ; Update retry
##     604800        ; Expiry
##     600           ; TTL for cache name server (for 30 minutes)
##   )
##
def search_old_serial(file_path):
    fp = open(file_path)
    lines = fp.readlines()
    fp.close()

    for line in lines:
        if line.find('Serial') > 0:
            words = line.split(';')
            old_serial = words[0].strip()
    return old_serial



zone_file = '/var/cache/bind/mydomain.kr.zone'

old_serial = search_old_serial(zone_file)
curr_date = datetime.today().strftime("%Y%m%d")
bool_today = old_serial.startswith(curr_date)
if bool_today is True:
    ## Serial 값이 오늘 Update된 경우
    int_old_serial = int(old_serial)
    new_serial = str(int_old_serial + 1)
else:
    ## Serial 값이 며칠 전에 Update된 경우
    new_serial = f"{curr_date}00"

## copy file for backup
bkup_file = f"{zone_file}_{new_serial}"
shutil.copy(zone_file, bkup_file)

replace_in_file(zone_file, old_serial, new_serial)


## 실행 테스트
$ ./update_zone_serial.py

 

 

Bind9 Zone 파일의 Serial 값을 변경하는 Bash Script

만약, Python 인터프리터가 없는 경우라면 아래와 같이 bash 스크립트를 작성해서 실행해도 동일한 결과를 얻을 수 있다.

$ cat my-dns-zone-serial-increase.sh


#!/usr/bin/env bash

set -euo pipefail

: ${1?"Usage: $0 <zone file>"}

IFILE=$1

if [ ! -w "${IFILE}" ]; then
    echo "Error cannot write to ${IFILE}"
    exit
fi

if [ ! -w $(pwd) ]; then
    echo "Error, sed needs write permission for temp file, add w to current directory"
    exit
fi

PREV_SERIAL=$(grep -i Serial "${IFILE}" | awk '{print $1}')
echo "PREV_SERIAL: ${PREV_SERIAL}"
TODAY=$(date +%Y%m%d00)

if [ "$PREV_SERIAL" -ge "${TODAY}" ]; then
    NEW_SERIAL=$((PREV_SERIAL+1))
else
    NEW_SERIAL=${TODAY}
fi

echo "NEW_SERIAL: ${NEW_SERIAL}"

sed -i "s/${PREV_SERIAL}/${NEW_SERIAL}/" "${IFILE}"

printf "Zone: %s [%d -> %d]\n" "${IFILE}" "${PREV_SERIAL}" "${NEW_SERIAL}"


## 실행 테스트
$ ./my-dns-zone-serial-increase.sh  /var/cache/bind/mydomain.kr.zone

 

 

 

 

 

 

 


 

반응형
테스트 및 블로그 작성한 날짜: 2023년 6월 13일

 

Prometheus 설치

아래 Web page에서 내 운영 환경에 맞는 파일을 다운로드한다.

https://prometheus.io/download/

 

아래와 같이 명령을 따라 수행하여 Prometheus 서버를 구동한다.

##
## Prometheus 서버 설치 파일을 다운로드
## 

$ wget https://github.com/prometheus/prometheus/releases/download/v2.45.0-rc.0/prometheus-2.45.0-rc.0.linux-amd64.tar.gz

##
## 압축 풀기
##

$ tar xf prometheus-2.45.0-rc.0.linux-amd64.tar.gz

$ cd prometheus-2.45.0-rc.0.linux-amd64

##
## 설정 파일 수정하기
##

$ cat prometheus.yml

# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    # 나는 아래 부분을 수정했다.
    # 10.1.4.56:8080은 Python으로 작성한 Exporter의 접속점이다.
    static_configs:
      - targets: ["10.1.4.56:8000", "10.1.3.241:9090"]
      
##
## Prometheus 서버 구동하기
##

$ ./prometheus --config.file="./prometheus.yml" &
... 중간 생략 ...
ts=2023-06-13T08:24:54.837Z caller=main.go:1004 level=info msg="Server is ready to receive web requests."
ts=2023-06-13T08:24:54.837Z caller=manager.go:995 level=info component="rule manager" msg="Starting rule manager..."

 

Prometheus Client 예제 작성

테스트용 Metric data를 만들기 위해서 아래와 같이 Example code를 작성한다.

from prometheus_client import start_http_server, Summary
from prometheus_client import Counter
import random
import time


# Create a metric to track time spent and requests made.
REQUEST_TIME = Summary('request_processing_seconds', 'Time spent processing request')

# Decorate function with metric.
@REQUEST_TIME.time()
def process_request(t):
    """A dummy function that takes some time."""
    time.sleep(t)


if __name__ == '__main__':
    # Start up the server to expose the metrics.
    start_http_server(8000)

    c = Counter('sejong_packet_bytes', 'http request failure', ['src_ip', 'dst_ip', 'src_port', 'dst_port'])
    # Generate some requests.
    while True:
        process_request(random.random()/10)
        c.labels(src_ip='10.1.4.11', dst_ip='192.168.5.22', src_port='11111', dst_port='').inc(1322)
        c.labels(src_ip='10.1.8.33', dst_ip='192.168.9.66', src_port='12345', dst_port='23456').inc(1500)
        c.labels(src_ip='172.16.8.33', dst_ip='192.168.9.66', src_port='12345', dst_port='23456').inc(1500)
        c.labels(src_ip='172.17.7.33', dst_ip='192.168.33.66', src_port='80808', dst_port='90909').inc(1500)

 

위에서 작성한 example code를 실행한다.

$ pip install prometheus-client

$ python3 my_example.py

 

좀 더 다양한 Python example code를 보려면, GitHub 저장소를 볼 것!

https://github.com/prometheus/client_python

 

Web UI에서 Metric 확인

Query 입력하는 공간에 "increase(sejong_packet_bytes_total[30s])" 를 입력한다.

 

 

블로그 작성자: sejong.jeonjo@gmail.com

 

반응형

 

YAML 내용 비교하기 (using Python)

 

 

`vim -d old_file new_file` 또는 diff 등 명령으로 파일에서 변경된 내용을 확인할 수 있지만,

이렇게 비교를 하면, 내용이 동일하더라도 특정 내용의 행이 위/아래로 이동한 경우에는 다른 파일로 간주된다.

즉, 내용을 비교하고 싶은데 문서의 Format이 변경된 것 때문에 다른 파일로 인식되는 문제가 생긴다.

 

따라서 서로 다른 버전의 YAML file을 비교할 때는 두 파일을 Parsing해서 구조체에 넣고, 특정 항목이 추가되었는지 또는 삭제되었는지 또는 변경되었는지를 일일히 확인하는 것이 제일 정확한다.

 

Istio project의 Utility code를 살펴보니, 이런 기능을 하는 python code가 있어서 인용해봤다.

 

https://github.com/istio/istio/blob/master/bin/diff_yaml.py

 

GitHub - istio/istio: Connect, secure, control, and observe services.

Connect, secure, control, and observe services. Contribute to istio/istio development by creating an account on GitHub.

github.com

 

 

 

혹시 GitHub에서 Folder 구조가 바뀌거나 diff_yaml.py 도구가 제거될 것을 우려해서 여기 blog에 흔적을 남긴다.

 

diff_yaml.py (source code 열람)

#!/usr/bin/env python
#
# Copyright 2018 Istio Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Compare 2 multi document kubernetes yaml files
# It ensures that order does not matter
#
from __future__ import print_function
import argparse
import datadiff
import sys
import yaml  # pyyaml

# returns fully qualified resource name of the k8s resource


def by_resource_name(res):
    if res is None:
        return ""

    return "{}::{}::{}".format(res['apiVersion'],
                               res['kind'],
                               res['metadata']['name'])


def keydiff(k0, k1):
    k0s = set(k0)
    k1s = set(k1)
    added = k1s - k0s
    removed = k0s - k1s
    common = k0s.intersection(k1s)

    return added, removed, common


def drop_keys(res, k1, k2):
    if k2 in res[k1]:
        del res[k1][k2]


def normalize_configmap(res):
    try:
        if res['kind'] != "ConfigMap":
            return res

        data = res['data']

        # some times keys are yamls...
        # so parse them
        for k in data:
            try:
                op = yaml.safe_load_all(data[k])
                data[k] = list(op)
            except yaml.YAMLError as ex:
                print(ex)

        return res
    except KeyError as ke:
        if 'kind' in str(ke) or 'data' in str(ke):
            return res

        raise


def normalize_ports(res):
    try:
        spec = res["spec"]
        if spec is None:
            return res
        ports = sorted(spec['ports'], key=lambda x: x["port"])
        spec['ports'] = ports

        return res
    except KeyError as ke:
        if 'spec' in str(ke) or 'ports' in str(ke) or 'port' in str(ke):
            return res

        raise


def normalize_res(res, args):
    if not res:
        return res

    if args.ignore_labels:
        drop_keys(res, "metadata", "labels")

    if args.ignore_namespace:
        drop_keys(res, "metadata", "namespace")

    res = normalize_ports(res)

    res = normalize_configmap(res)

    return res


def normalize(rl, args):
    for i in range(len(rl)):
        rl[i] = normalize_res(rl[i], args)

    return rl


def compare(args):
    j0 = normalize(list(yaml.safe_load_all(open(args.orig))), args)
    j1 = normalize(list(yaml.safe_load_all(open(args.new))), args)

    q0 = {by_resource_name(res): res for res in j0 if res is not None}
    q1 = {by_resource_name(res): res for res in j1 if res is not None}

    added, removed, common = keydiff(q0.keys(), q1.keys())

    changed = 0
    for k in sorted(common):
        if q0[k] != q1[k]:
            changed += 1

    print("## +++ ", args.new)
    print("## --- ", args.orig)
    print("## Added:", len(added))
    print("## Removed:", len(removed))
    print("## Updated:", changed)
    print("## Unchanged:", len(common) - changed)

    for k in sorted(added):
        print("+", k)

    for k in sorted(removed):
        print("-", k)

    print("##", "*" * 25)

    for k in sorted(common):
        if q0[k] != q1[k]:
            print("## ", k)
            s0 = yaml.safe_dump(q0[k], default_flow_style=False, indent=2)
            s1 = yaml.safe_dump(q1[k], default_flow_style=False, indent=2)

            print(datadiff.diff(s0, s1, fromfile=args.orig, tofile=args.new))

    return changed + len(added) + len(removed)


def main(args):
    return compare(args)


def get_parser():
    parser = argparse.ArgumentParser(
        description="Compare kubernetes yaml files")

    parser.add_argument("orig")
    parser.add_argument("new")
    parser.add_argument("--ignore-namespace", action="store_true", default=False,
                        help="Ignore namespace during comparison")
    parser.add_argument("--ignore-labels", action="store_true", default=False,
                        help="Ignore resource labels during comparison")
    parser.add_argument("--ignore-annotations", action="store_true", default=False,
                        help="Ignore annotations during comparison")

    return parser


if __name__ == "__main__":
    parser = get_parser()
    args = parser.parse_args()
    sys.exit(main(args))

 

 

diff_yaml.py 명령 따라하기

위 python code를 diff_yaml.py 파일명으로 저장하고, 아래와 같이 필요한 Python package를 설치하고, Run 한다.

$  pip install --upgrade pip

$  python -m pip install datadiff

$  ./diff_yaml.py /tmp/metallb-1.yaml /tmp/metallb-2.yaml

## +++  /tmp/metallb-2.yaml
## ---  /tmp/metallb-1.yaml
## Added: 1
## Removed: 1
## Updated: 0
## Unchanged: 14
+ apps/v1::Deployment_Test::controller     ## 변경된 내용을 보여준다.
- apps/v1::Deployment::controller          ## 변경된 내용을 보여준다.
## *************************

$

## 이것은 Linux diff 명령으로 확인한 내용이다.
## 실제로 yaml 내용은 변경된 것이 1개 이지만, 단지 행을 이동했다는 이유만으로
## 여러 곳이 다르다고 표기하고 있다. (즉, YAML을 사용하는 User 관점에서 보면 다 쓸데 없는 정보이다.)

$  diff /tmp/metallb-1.yaml /tmp/metallb-2.yaml
3d2
< kind: Namespace             ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
5d3
<   name: metallb-system      ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
8a7,8
>   name: metallb-system      ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
> kind: Namespace             ## <-- 실제로 이 부분은 내용이 바뀐 것이 아니라, 라인만 이동한 것이다.
352d351
< kind: Deployment
400a400
> kind: Deployment_Test       ## <-- 이 부분만 내용이 변경된 것이다.

 

위 결과를 보면, 일반적인 diff와 내용이 다르다는 것을 알 수 있다.

가장 큰 차이점은 diff_yaml.py는 수정된 Line과 Column을 출력하지 않는다. 왜냐하면, 내용 자체의 변경 여부가 관심사이지 어떤 내용이 다른 행으로 이동했는지 또는 삭제, 추가되었는지 중요하지 않기 때문이다.

 

 

 

JSON 내용 비교하기 (using Python)

 

JSON 내용을 비교하는 CLI 도구는 아래 Web Docs를 참고하길 ~~~

 

 

GitHub - xlwings/jsondiff: Diff JSON and JSON-like structures in Python

Diff JSON and JSON-like structures in Python. Contribute to xlwings/jsondiff development by creating an account on GitHub.

github.com

 

 

json-diff

Generates diff between two JSON files

pypi.org

 

 

+ Recent posts