반응형
작성일: 4월 15일

 

 

Kubernetes를 사용하면서 만나는 문제 상황

Kubernetes service resource를 삭제하고 다시 생성하면, Public IP(또는 EXTERNAL-IP) 값이 변경된다.

AWS, Oracle Cloud, Azure, GCP(Google Cloud Platform) 같은 Public Cloud Infra에서 제공하는 

Reserved Public IP 기능과 Service YAML에 'externalIPs' 값을 설정하면 Public IP Address를 고정시킬 수 있지만,

Helm Chart를 작성하거나 Kubernetes Service Manifest 파일을 작성할 때마다 

  1. Reserved Public IP 생성 요청
  2. 위에서 생성된 Public IP 값을 확인 후 Service Manifest 작성(또는 Helm chart) 작성

한다는 것은 참 불편한 작업이다.

 

 

그래서 아래의 기능을 수행하는 Python script를 만들었다.

 

  1. my-conf.yaml 파일에서 Public IP address 정보를 갱신할 'Kubernetes service' 정보와 'Internet domain name' 목록을 설정
  2. DNS 서버에서 Kubernetes API server에게 Service resource의 public IP address를 조회 요청
  3. 이렇게 얻은 public IP address(즉, EXTERNAL-IP)를 자동으로 DNS 서버의 Zone file에 반영
  4. BIND9 서비스 데몬을 재기동

 

아래 예제 설정과 Python 소스 코드를 이용하면 잘 동작함 ^^

 

설정 파일 작성  [ 파일명: my-conf.yaml ]

 

$ cat my-conf.yaml

zone_file: /var/cache/bind/my-domain.kr.zone
json_file: /var/cache/bind/my-domain.kr.json

domain_name: my-domain.kr

services:
  - kube_svc_name: almighty
    host_name: almighty
  - kube_svc_name: myhappyserver
    host_name: myhappynacserver
  - kube_svc_name: my-ingress-nginx-controller
    host_name: myproxyserver

 

 

 

BIND9 Zone File 생성을 위한 Jinja2 Template 파일 작성  [ 파일명: zone.j2 ]

 

$ cat zone.j2

; ------------------------------------------------------------------------------------------
; NOTE:
;   이 파일은 대한민국 서울에 사는 Andrew Jeong이 만든 "DNS Auto Configuration Application"이
;   자동으로 생성한 파일입니다.
;   사용자가 이 파일을 직접 편집해도 일정 시간 이후에 Application에 의해서 재작성될 것입니다.
;   따라서 이 Zone file을 직접 편집하는 것을 금합니다.
;   만약, Zone file에 반영할 내용이 있다면 같은 폴더에 있는 JSON 파일을 수정해야 합니다.
; ------------------------------------------------------------------------------------------

; Internet Domain Zone :  {{ origin }}
; Exported date        :  {{ now }}

$ORIGIN {{ origin }}
$TTL {{ ttl }}

;
; SOA Record
;
@    IN    SOA    {{ soa['mname'] }}    {{ soa['rname'] }} (
    {{ soa['serial'] }} 	; serial
    {{ soa['refresh'] }} 	; refresh
    {{ soa['retry'] }} 	; retry
    {{ soa['expire'] }} 	; expire
    {{ soa['minimum'] }} 	; minimum ttl
)


{% if ns is defined -%}
;
; NS Records
;
{% for entry in ns -%}
@   IN  NS  {{ entry['host'] }}
{% endfor %}
{% endif %}


{%- if mx is defined -%}
;
; MX Records
;
{% for entry in mx -%}
@	IN	MX	{{ entry['preference'] }}    {{ entry['host'] }}
{% endfor %}
{% endif %}


{%- if spf is defined -%}

; SPF Records
{% for entry in spf -%}
{{ entry['domain'] }}		IN	TXT	"{{ entry['record'] }}"
{% endfor %}
{% endif %}


{%- if a is defined -%}
;
; A Records
;
{% for entry in a -%}
	{{ "{:<17}".format(entry['name']) }}  IN  A      {{ entry['ip'] }}
{% endfor %}
{% endif %}


{%- if aaaa is defined -%}

; AAAA Records
{%- for entry in aaaa -%}
{{ entry['name'] }}		IN	AAAA	{{ entry['ip'] }}
{% endfor %}
{% endif %}


{%- if cname is defined -%}
;
; CNAME Records
;
{% for entry in cname -%}
	{{ "{:<17}".format(entry['name']) }}  IN  CNAME  {{ entry['alias'] }}
{% endfor %}
{% endif %}


{%- if ptr is defined -%}
;
; PTR Records
;
{% for entry in ptr -%}
{{ entry['name'] }}		IN	PTR	"{{ entry['host'] }}"
{% endfor %}
{% endif %}


{%- if txt is defined -%}
;
; TXT Records
;
{% for entry in txt -%}
{{ entry['name'] }}		IN	TXT	"{{ entry['txt'] }}"
{% endfor %}
{% endif %}


{%- if srv is defined -%}
;
; SRV Records
;
{% for entry in srv -%}
{{ entry['name'] }}		IN	SRV	{{ entry['priority'] }}   {{ entry['weight'] }} {{ entry['port'] }}   {{ entry['target'] }}
{% endfor %}
{%- endif %}

; End of Zone

 

 

 

BIND9 Zone File 생성을 위한 JSON 파일 작성  [ 파일명: /var/cache/bind/my-domain.kr.json ]

$ cat /var/cache/bind/my-domain.kr.json

{
    "origin": "my-domain.kr.",
    "ttl": 600,
    "soa": {
        "mname": "ns.my-domain.kr.",
        "rname": "sejong.my-domain.kr.",
        "serial": "2024041500",
        "refresh": 3600,
        "retry": 600,
        "expire": 604800,
        "minimum": 86400
    },
    "ns": [
        {
            "host": "ns.my-domain.kr."
        },
        {
            "host": "ns1.my-domain.kr."
        }
    ],
    "a": [
        {
            "name": "@",
            "ip": "16.6.80.3"
        },
        {
            "name": "ns",
            "ip": "16.6.80.3"
        },
        {
            "name": "ns1",
            "ip": "13.2.88.15"
        },
        {
            "name": "myhappyserver",
            "ip": "13.93.28.9"
        },
        {
            "name": "my-ingress-nginx-controller",
            "ip": "10.1.7.6"
        },
        
        ... 중간 생략 ...
        
        {
            "name": "almighty",
            "ip": "129.154.195.186"
        }
    ],
    "cname": [
        {
            "name": "my-nicname-server",
            "alias": "almighty"
        },
        {
            "name": "toy",
            "alias": "my-ingress-nginx-controller"
        },
        
        ... 중간 생략 ...
        
        {
            "name": "my-supersvc",
            "alias": "mail.sogang.ac.kr"
        }
    ]
}

 

 

로그 파일 생성을 위한 Python Script 작성  [ 파일명: my_log.py ]

$ cat my_log.py

#!/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}")

 

 

Kubernetes External-IP를 조회 및 DNS 자동 설정을 위한 Python Script 작성 [  파일명: kube-dns-auto-config.py ]

 

#!/usr/bin/python3

import os
import time
import socket
import shutil
import subprocess
import yaml
import json
import datetime
#from datetime import datetime
from kubernetes import client, config
from jinja2 import Environment, FileSystemLoader

import my_log


###############################################################
## NOTE: You have to fix the following variable 'my_run_dir'
###############################################################
my_run_dir = "/opt/kube-dns-auto-config/"


def search_host_from_json_data(host_name):
    for item in json_data['a']:
        if item['name'] == host_name:
            return item
    return None


def chk_ip_addr_changed(conf_svc_item):
    kube_svc_name = conf_svc_item["kube_svc_name"]
    host_name = conf_svc_item["host_name"]

    log.logger.info(f"\n")
    log.logger.info(f"[ Checking configuration '{kube_svc_name}' / '{host_name}' ]")

    for item in kube_svc_info.items:
        if item.metadata.name == kube_svc_name:
            ip_addr_k8s_svc = item.status.load_balancer.ingress[0].ip
            log.logger.info(f"  {kube_svc_name:<11} from K8S SVC EXT-IP : {ip_addr_k8s_svc:>15}")

            dns_record = search_host_from_json_data(host_name)
            if dns_record == None:
                log.logger.info("  host_name {host_name} is not found.")
                return 0
            log.logger.info(f"  {host_name:<11} from JSON file      : {dns_record['ip']:>15}")
            if ip_addr_k8s_svc == dns_record['ip']:
                log.logger.info("  IP address of domain name is not changed. Nothing to do.")
                return 0
            else:
                log.logger.info(f"  IP address of domain name is changed;   {dns_record['ip']} -> {ip_addr_k8s_svc}")
                dns_record['ip'] = ip_addr_k8s_svc
                return 1
    return 0



def get_new_serial(old_serial):
    curr_date = datetime.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"

    log.logger.info(f"old_serial: [{old_serial}]")
    log.logger.info(f"new_serial: [{new_serial}]")

    return new_serial


def make_bkup_file(serial):
    zone_file = my_conf_info['zone_file']
    bkup_zone_file = f"{zone_file}_{serial}"
    shutil.copy(zone_file, bkup_zone_file)

    bkup_json_file = f"{json_file}_{serial}"
    shutil.copy(json_file, bkup_json_file)

    return None


def make_zone_file():
    ## Jinja2 environment object and refer to templates directory
    env = Environment(loader=FileSystemLoader(my_run_dir))
    template = env.get_template('zone.j2')

    template.globals['now'] = datetime.datetime.now().isoformat()
    rendered_data = template.render(json_data)

    log.logger.info(f"rendered_data: \n{rendered_data}")

    fp = open(my_conf_info['zone_file'], "w")
    fp.write(rendered_data)
    fp.close()

    return None


## ------------------------------------------------------------------------
##     Begin of Main
## ------------------------------------------------------------------------

if __name__ == '__main__':
    log = my_log.MyLog("log_kube_dns", logname="kb_log")
    log.logger.info("#####  Start program  #####")

    ## Load configration from YAML file.
    conf_path = my_run_dir + "my-conf.yaml"
    with open(conf_path) as fp:
        my_conf_info = yaml.load(fp, Loader = yaml.FullLoader)

    json_file = my_conf_info['json_file']

    ## Configs can be set in Configuration class directly or using helper utility
    config.load_kube_config()
    v1 = client.CoreV1Api()

    ##
    ## !!! Main service logic !!!
    ##
    while True:
        svc_ip_changed = 0

        with open(json_file) as json_fp:
            json_data = json.load(json_fp)

        kube_svc_info = v1.list_service_for_all_namespaces(watch=False)
        for conf_svc_item in my_conf_info['services']:
            svc_ip_changed += chk_ip_addr_changed(conf_svc_item)

        if svc_ip_changed > 0:
            log.logger.info(f"IP address is changed")
            log.logger.info(f"json_data: \n\n{json_data}\n")

            new_serial = get_new_serial(json_data['soa']['serial'])
            json_data['soa']['serial'] = new_serial

            ## BKUP file 만들기
            make_bkup_file(new_serial)

            ## JSON 파일에 변경된 내용을 Update한다.
            json_str = json.dumps(json_data, indent=4)
            with open(json_file, "w") as json_outfile:
                json_outfile.write(json_str)

            ## JSON Data를 Zone DB 포맷으로 변환하여 Zone file에 저장
            make_zone_file()

            ## Restart BIND9 service
            ret_value = subprocess.call("systemctl restart bind9", shell=True)
            log.logger.info(f"\n\n")
            log.logger.info(f"[ BIND9 is restarted ] 'systemctl' return: {ret_value}\n\n")
        time.sleep(60)

    ## ------------------------------------------------------------------------
    ##     End of Main
    ## ------------------------------------------------------------------------

 

 

 

실행하기 (테스트하기)

$ /opt/kube-dns-auto-config/kube-dns-auto-config.py

 

 

위와 같이 Python script를 실행하고, 로그 파일을 열람한다.

$ tail -f /opt/kube-dns-auto-config/log_kube_dns/kb_log/kb_log.log

 

 

 

 

 

주의 사항

위 Python script를 실행하는 DNS 서버 장비에 $KUBE_CONFIG 파일이 존재해야 한다.

예를 들어, /home/sejong/.kube/config 같은 파일이 있어야 이 Python 프로그램이 Kubernetes API server에 접근이 가능하다.

 

 

 


 

반응형

 

 

작성일: 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년 4월 15일

 

 

Kubernetes를 사용하면서 만나는 문제 상황

Kubernetes service resource를 삭제하고 다시 생성하면, Public IP(또는 EXTERNAL-IP) 값이 변경된다.

AWS, Oracle Cloud, Azure, GCP(Google Cloud Platform) 같은 Public Cloud Infra에서 제공하는 

Reserved Public IP 기능과 Service YAML에 'externalIPs' 값을 설정하면 Public IP Address를 고정시킬 수 있지만,

Helm Chart를 작성하거나 Kubernetes Service Manifest 파일을 작성할 때마다 

  1. Reserved Public IP 생성 요청
  2. 위에서 생성된 Public IP 값을 확인 후 Service Manifest 작성(또는 Helm chart) 작성

한다는 것은 참 불편한 작업이다.

 

 

그래서 아래의 기능을 수행하는 Python script를 만들었다.

 

  1. my.yaml 파일에서 Public IP address 정보를 갱신할 'Kubernetes service' 정보와 'Internet domain name' 목록을 설정
  2. DNS 서버에서 직접 Kubernetes API server에게 매번 변경되는 Service resource의 public IP address를 조회 요청
  3. 이렇게 얻은 public IP address(즉, EXTERNAL-IP)를 자동으로 DNS 서버의 Zone file에 반영
  4. BIND9 서비스 데몬을 재기동

 

#!/usr/bin/python3


##############################################################
##
## Written by "Andrew Jeong"
## Date: 2024.03.25
## File: kube-dns-auto-config.py
##############################################################



import os
import time
import socket
import shutil
import subprocess
import yaml
from datetime import datetime
from kubernetes import client, config


##
## 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:
        # print(f"line: {line}")
        if line.find('Serial') > 0:
            # print(f"I found serial value:  {line}")
            words = line.split(';')
            old_serial = words[0].strip()
            # print(f"old_serial: [{old_serial}]")
    return old_serial



##
## Update 'serial number' of zone file
##
def update_serial_for_zone_file(file_path):
    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"

    print(f"old_serial: [{old_serial}]")
    print(f"new_serial: [{new_serial}]")

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

    ## Update serial number
    replace_in_file(zone_file, old_serial, new_serial)



def update_dns(conf):
    kube_svc_name = conf['kube_svc_name']
    kube_domain_name = conf['kube_domain_name']

    print(f"\n[ Checking configuration '{kube_svc_name}' / '{kube_domain_name}' ]")

    for item in svc_info.items:
        if item.metadata.name == kube_svc_name and item.metadata.namespace:
            ip_k8s_svc = item.status.load_balancer.ingress[0].ip
            print(f"  Kubernetes Service External-IP: {ip_k8s_svc}")

    ip_addr_domain_name = socket.gethostbyname(kube_domain_name)
    print(f"  {kube_domain_name}: {ip_addr_domain_name}")

    if ip_addr_domain_name == ip_k8s_svc:
        print("  IP address of domain name is not changed. Nothing to do.")
        return -1
    else:
        update_serial_for_zone_file(zone_file)
        print("  IP address of domain name is changed.")
        print("  Make a backup file of zone file and then")
        print(f"  update '{kube_domain_name} IN A {ip_addr_domain_name}' record")
        print(f"    {ip_addr_domain_name} -> {ip_k8s_svc}")
        replace_in_file(zone_file, ip_addr_domain_name, ip_k8s_svc)
        return 0





## ------------------------------------------------------------------------
##     Begin of Main
## ------------------------------------------------------------------------

## Load configration from YAML file.
with open('/root/WorkSpace/kube-python-client/my.yaml') as fp:
    my_conf_info = yaml.load(fp, Loader = yaml.FullLoader)

zone_file = my_conf_info['zone_file']

##
## Get new IP address from Kubernetes service resource
## and
## Get old IP address from DNS
##

## Configs can be set in Configuration class directly or using helper utility
config.load_kube_config()
v1 = client.CoreV1Api()

while True:
    svc_restart_flag = 0
    svc_info = v1.list_service_for_all_namespaces(watch=False)
    for conf in my_conf_info['services']:
        ret = update_dns(conf)
        if ret == 0:
            svc_restart_flag = 1
    if svc_restart_flag == 1:
        ret_value = subprocess.call("systemctl restart bind9", shell=True)
        print(f"\n\n[ BIND9 is restarted ] Return: {ret_value}\n\n")
    time.sleep(300)

## ------------------------------------------------------------------------
##     End of Main
## ------------------------------------------------------------------------

 

 

아래 예시와 같이 my.yaml 설정 파일을 작성한다.

 

## File: my.yaml

zone_file: /var/cache/bind/mydomain.kr.zone
services:
  - kube_svc_name: almighty
    kube_domain_name: almighty.mydomain.kr
  - kube_svc_name: my-test-server
    kube_domain_name: my-test-server.mydomain.kr

 

참고:
'kube_domain_name'은 실제로 존재하는 domain_name 이어야 한다.
YesNIC, Whois 같은 도메인 등록 기관에서 등록 후 위 테스트를 진행해야 한다.

 

 

 

아래 예시와 같이 실행하면 자동으로 Kubernetes cluster에 있는 Service resource 정보를 반영하여

DNS Zone file이 수정될 것이다.

 

$ ./kube-dns-auto-config.py

[ Checking configuration 'almighty' / 'almighty.mydomain.kr' ]
  Kubernetes Service External-IP: 146.53.17.8
  almighty.mydoamin.kr: 143.26.1.4
  ... 중간 생략 ...

[ Checking configuration 'my-test-server' / 'my-test-server.mydomain.kr' ]
  Kubernetes Service External-IP: 193.13.25.23
  my-test-server.mydomain.kr: 193.13.25.23
  ... 중간 생략 ...
  ...
  ...

 

 

아래와 같이 Kubernetes cluster에 있는 almighty 서비스로 HTTP 트래픽을 보내보면, 트래픽을 잘 갈거다.

$ curl -v almighty.mydomain.kr
... 내용 생략 ...

 

 

 


 

반응형

 


 

작성일: 2023년 12월 12일

 

 

Juniper Switch를 원격에서 제어할 수 있는 API를 찾아보니 아래와 같은 것들이 있었다.

- Python PKG (Juniper가 공식적으로 개발 및 배포하고 Example이 다양하게 많아서 사용하기 좋았다.)

- Ansible Module (Juniper가 공시적으로 배포하지만, 실전에서 사용하기에는 Example이 부족했다)

- SNMP (Juniper가 SNMP 서비스를 제공하지만, 제약이 많다)

- NET-CONF (업계 표준이니까 제공해주지만, 쌩코딩해서 사용하기에는 시간적으로 부담이 된다)

 

구현 시간을 아끼고, 예제도 많은 Python PKG를 선택해서 개발하기로 마음을 굳혔다.

그리고 나중에 시간이 생기면 Ansible playbook을 작성해서 조금 더 추상화해볼 계획이다.


 

 

참고:

아래의 예제는 [ Junos PyEX 개발자 가이드 ] 문서 중에서 업무에 필요한 부분만 추려서 실습하고 작성한 것이다.

시간이 넉넉한 사람은 아래 개발자 가이드를 다 읽어보고 Junos PyEX를 사용하고,

시간이 넉넉하지 않은 사람은 아래 요약된 실습 내용만 따라하면 된다.

https://www.juniper.net/documentation/kr/ko/software/junos-pyez/junos-pyez-developer/index.html

 

 

 

준비 작업 / Juniper Switch 장비 설정 작업

Client 장비의 Junos PyEX가 접근할 수 있도록 Juniper Switch에서는 [ SSH 서비스, NETCONF, Junos XML API ]를 활성화(Enable)해야 한다.

설명은 거창하지만, 아래의 예제 명령을 한줄 입력해주면 끝이다.

 

> configure         // 설정 모드로 진입하기 위한 명령을 입력

Entering configuration mode
Users currently editing the configuration:
... 중간 생략 ...

myuser# set netconf ssh   // NETCONF-over-SSH 서비스를 활성화하는 명령을 입력

myuser# commit            // 위에서 변경한 내용을 시스템에 반영하기 위한 명령을 입력

 

 

Junos PyEX 패키지(jnpr.junos)의 각 모듈에 대한 설명

Module Description
device Junos 디바이스를 표현하는 클래스를 정의하고, Device spec 및 상태를 조회할 수 있는 기능을 지원한다.
command CLI, vty 명령을 지원한다. 
표 형태의 View 출력 Form을 지원한다.
exception Exception handling
factory 사용자가 지정한 형태의 Table, View 출력이 가능하도록 한다.
  예: loadyaml() 함수
facts Device에 대한 정보를 추출하기 위한 Object
op RPC 응답의 XML 출력을 Filtering
resources ... 설명 생략 ...
transport ... 설명 생략 ...
utils Configuration utility, Shell utility, Software installation, Security utility, etc ...

 

준비 작업 / Client 장비에 Python 패키지 설치 작업

내가 사용한 OS: Ubuntu 22.04

 

Juniper Switch를 관리할 Client 장비(Ubuntu 22.04)에서 아래 명령을 수행하여 Junos PyEX 패키지를 설치한다.

$ sudo pip3 install junos-eznc

 

작업 끝. (너무 간단하다 ^^)

 

[ 예제 1 ]  네트워크 포트 Up & Down 예제 프로그램

아래 예제는 Juniper Switch(10.1.1.2) 장치에 접속해서 Giga Ethernet을 Down & Up 상태로 변경하고

Port 상태를 조회하는 기능을 수행한다.

 

아래와 같이 Python 예제 코드를 작성한다.

from jnpr.junos import Device
from jnpr.junos.utils.config import Config
from jnpr.junos.op.ethport import EthPortTable
import time


with Device(host='10.1.1.2', user='myadmin', password='mypasswd') as dev:
    ## 포트 상태를 Up/Down 상태로 변경하기
    with Config(dev, mode='ephemeral') as cu:
        cu.load('set interfaces ge-0/0/8 disable', format='set')      # Port Down 상태로 변경
        #cu.load('delete interfaces ge-0/0/8 disable', format='set')  # Port Up 상태로 변경
        cu.commit()

    time.sleep(5)   ## Config를 commit하고도 2~3초 정도 지나야 Port state 값이 반영되므로 5초 정도 sleep 한다.
    
    ## 전체 포트 상태 정보를 가져오기
    eths = EthPortTable(dev)
    eths.get()
    for item in eths:
        print ("{}: {}".format(item.name, item.oper))

    # 1개 포트 정보만 가져오기
    eths.get("ge-0/0/8")
    # 포트 정보의 모든 컬럼의 내용을 보고 싶다면...
    print(eths.values())
    for item in eths:
        print ("name: {}  operating state: {}  admin state: {}  mac: {}  mtu: {}".format(item.name, item.oper, item.admin, item.macaddr, item.mtu))

 

 

위에서 작성한 Python 예제 코드를 아래와 같이 실행한다.

$  python3 my_example_1.py

ge-0/0/0: down
ge-0/0/1: down
ge-0/0/2: down
ge-0/0/3: down
ge-0/0/4: down
ge-0/0/5: down
ge-0/0/6: down
ge-0/0/7: down
ge-0/0/8: up         <-- 내가 변경한 포트의 상태
ge-0/0/9: down
ge-0/0/10: down
ge-0/0/11: down
ge-0/0/12: down
ge-0/0/13: down
ge-0/0/14: down
ge-0/0/15: down
ge-0/0/16: down
ge-0/0/17: down
ge-0/0/18: down
ge-0/0/19: down
ge-0/0/20: down
ge-0/0/21: down
ge-0/0/22: down
ge-0/0/23: up

[[('oper', 'up'), ('admin', 'up'), ('description', None), ('mtu', 1514), ('link_mode', 'Full-duplex'), ('macaddr', '60:c7:8d:a3:78:74'), ('rx_bytes', '2124'), ('rx_packets', '24'), ('tx_bytes', '586755785'), ('tx_packets', '7013363'), ('running', True), ('present', True)]]

name: ge-0/0/8  operating state: up  admin state: up  mac: 60:c3:8a:a3:78:74  mtu: 1514

$

 

 

 


 

[ 예제 2 ]  Juniper Switch의 VLAN 별, 포트 별 Learning된 MAC Address List를 조회

아래 예제는 Juniper Switch(10.1.1.2) 장치에 접속해서 Juniper Switch가 MAC Learning한 결과물을 조회한다.

Learning된 MAC Address를 각각의 VLAN 별, 포트별로 표현된다.

 

조회 결과물이 XML이기 때문에 XML 데이터 포맷을 처리하기 위한 Python 패키지가 필요한다.

$ pip install xmltodict

 

 

아래와 같이 Python 예제 코드를 작성한다.

import pprint
import xmltodict
from xml.etree import ElementTree
from jnpr.junos import Device
from jnpr.junos.op.ethernetswitchingtable import EthernetSwitchingTable


with Device(host='10.1.1.2', user='myadmin', password='mypasswd') as dev:
    eth_table = dev.rpc.get_ethernet_switching_table_information()
    dict_eth_table = xmltodict.parse(ElementTree.tostring(eth_table, method="xml"))
    pprint.pprint(dict_eth_table, indent=2)

 

 

위에서 작성한 Python 예제 코드를 아래와 같이 실행한다.

$  python3  my_example_2.py

{ 'l2ng-l2ald-rtb-macdb':
    { 'l2ng-l2ald-mac-entry-vlan':
        { '@style': 'brief-rtb',
            'l2ng-l2-mac-routing-instance': 'default-switch',
            'l2ng-l2-vlan-id': '100',
            'l2ng-mac-entry': [
                { 'l2ng-l2-mac-address': '00:03:51:17:07:14',
                  'l2ng-l2-mac-age': '-',
                  'l2ng-l2-mac-flags': 'D',
                  'l2ng-l2-mac-fwd-next-hop': '0',
                  'l2ng-l2-mac-logical-interface': 'ge-0/0/23.0',
                  'l2ng-l2-mac-rtr-id': '0',
                  'l2ng-l2-mac-vlan-name': 'vlan-100'},
                { 'l2ng-l2-mac-address': '00:03:51:20:02:e2',
                  'l2ng-l2-mac-age': '-',
                  'l2ng-l2-mac-flags': 'D',
                  'l2ng-l2-mac-fwd-next-hop': '0',
                  'l2ng-l2-mac-logical-interface': 'ge-0/0/23.0',
                  'l2ng-l2-mac-rtr-id': '0',
                  'l2ng-l2-mac-vlan-name': 'vlan-100'},

                ... 중간 생략 ...

                { 'l2ng-l2-mac-address': 'fc:34:51:b6:35:eb',
                  'l2ng-l2-mac-age': '-',
                  'l2ng-l2-mac-flags': 'D',
                  'l2ng-l2-mac-fwd-next-hop': '0',
                  'l2ng-l2-mac-logical-interface': 'ge-0/0/23.0',
                  'l2ng-l2-mac-rtr-id': '0',
                  'l2ng-l2-mac-vlan-name': 'vlan-100'},
                { 'l2ng-l2-mac-address': 'fc:34:51:e1:35:ea',
                  'l2ng-l2-mac-age': '-',
                  'l2ng-l2-mac-flags': 'D',
                  'l2ng-l2-mac-fwd-next-hop': '0',
                  'l2ng-l2-mac-logical-interface': 'ge-0/0/23.0',
                  'l2ng-l2-mac-rtr-id': '0',
                  'l2ng-l2-mac-vlan-name': 'vlan-100'}
            ],
            'learnt-mac-count': '59',
            'mac-count-global': '59'
        }
    }
}

$

 

 


 

[ 예제 3 ]  Juniper Switch의  장치 정보, Spec 등 조회

아래 예제는 Juniper Switch(10.1.1.2) 장치에 접속해서 Juniper Switch의 장치 정보 및 Spec을 조회한다. (아래 항목을 참고)

- Switch model name

- Switch status

- 기동하여 운영되고 있는 시간 (up time)

- Switch 장비 이중화 정보 (Master, Member, FWDD 등)

- FQDN (domain name)

- Switch 장치의 Hostname

- JunOS 정보 (os name, version)

- etc

 

 

아래와 같이 Python 예제 코드를 작성한다.

from pprint import pprint
from jnpr.junos import Device
from jnpr.junos import Device

with Device(host='10.1.1.2', user='myuser', password='mypasswd1234' ) as dev:
    pprint(dev.facts)
    pprint(dev.facts['version'])
    pprint(dev.facts['switch_style'])

 

 

위에서 작성한 Python 예제 코드를 아래와 같이 실행한다.

$  python3  my_example_3.py

{'2RE': False,
 'HOME': '/var/home/admin',
 'RE0': {'last_reboot_reason': '0x1:power cycle/failure',
         'mastership_state': 'master',
         'model': 'RE-EX2300-24T',
         'status': 'OK',
         'up_time': '6 days, 23 hours, 50 minutes, 36 seconds'},
 'RE1': None,
 'RE_hw_mi': False,
 'current_re': ['master',
                'node',
                'fwdd',
                'member',
                'pfem',
                're0',
                'fpc0',
                'localre'],
                
... 중간 생략 ...

 'version': '20.4R3-S2.6',
 'version_info': junos.version_info(major=(20, 4), type=R, minor=3-S2, build=6),
 'virtual': False}
'20.4R3-S2.6'
'VLAN_L2NG'

$

 

 


 

[ 예제 4 ]  Juniper Switch의  Shell 명령, CLI 명령을 원격으로 실행

아래 예제는 Juniper Switch(10.1.1.2) 장치에 접속해서 Juniper Switch의 shell 명령(CLI 명령)을 실행하는 코드이다.

 

아래와 같이 Python 예제 코드를 작성한다.

from jnpr.junos import Device
from jnpr.junos.utils.start_shell import StartShell


dev = Device(host='10.1.1.2', user='myadmin', password='mypasswd1234' )

ss = StartShell(dev)
ss.open()

version = ss.run('cli -c "show version | no-more"')
print(version)

ether_tbl = ss.run('cli -c "show ethernet-switching table interface ge-0/0/23 | no-more"')
print(ether_tbl)

ss.close()

 

 

위에서 작성한 Python 예제 코드를 아래와 같이 실행한다.

$  python3  my_example_4.py

True, 'cli -c "show version | no-more"\r\r\n
 fpc0:\r\n
 --------------------------------------------------------------------------\r\n
 Model: ex2300-24t\r\n
 Junos: 20.4R3-S2.6\r\n
 JUNOS OS Kernel 32-bit  [20211117.c779bdc_builder_stable_11-204ab]\r\n
... 중간 생략 ...
 '
)


(True, 'cli -c "show ethernet-switching table interface ge-0/0/23 | no-more"\r\r\n\r\n
 MAC database for interface ge-0/0/23\r\n\r\n
 MAC database for interface ge-0/0/23.0\r\n\r\n
 MAC flags (S - static MAC, D - dynamic MAC, L - locally learned, P - Persistent static, C - Control MAC\r\n
           SE - statistics enabled, NM - non configured MAC, R - remote PE MAC, O - ovsdb MAC)\r\n\r\n\r\n
 Ethernet switching table : 61 entries, 61 learned\r\n
 Vlan                MAC                 MAC         Age    Logical                NH        RTR \r\n
 name                address             flags              interface              Index     ID\r\n
 default             00:03:5a:17:07:14   D             -   ge-0/0/23.0            0         0       \r\n
 default             00:03:5a:20:02:e2   D             -   ge-0/0/23.0            0         0       \r\n
... 중간 생략 ...
 default             fc:34:5a:e1:35:ea   D             -   ge-0/0/23.0            0         0       \r\n% '
)

$

 

 

 

 

 

 

 

 

참고 문서

Junos PyEX 파이썬 패키지를 사용하여 Juniper Switch 구성 정보를 가져오기

Junos PyEX 파이썬 패키지를 사용하여 출력 내용을 구조화하기 (출력할 컬럼을 선별하여 요약본 만들기)

 

 

 

 

 


 

반응형

작성일: 2023년 9월 25일

 

파이썬으로 작성된 패킷 조작 라이브러리.

https://scapy.net/

 

 

 

 

Scopy - README.MD

https://github.com/secdev/scapy

 

Tutorial

https://scapy.readthedocs.io/en/latest/usage.html#interactive-tutorial

 

Scopy in 15 minutes

https://github.com/secdev/scapy/blob/master/doc/notebooks/Scapy%20in%2015%20minutes.ipynb

 

HTTP/2 Tutorial

https://github.com/secdev/scapy/blob/master/doc/notebooks/HTTP_2_Tuto.ipynb

 

Demo

https://scapy.readthedocs.io/en/latest/introduction.html#quick-demo

 

 

 

 

 

 


 

반응형

 

작성일: 2023년 9월 19일

 

NETCONF Python 예제 코드 

https://github.com/ncclient/ncclient/tree/master/examples

https://github.com/ncclient/ncclient/blob/master/examples/base/nc08.py  (Interface 설정 및 Activate)

https://github.com/aristanetworks/openmgmt/tree/main/src/ncclient  (Aristanetworks 예제)

https://developer.cisco.com/codeexchange/github/repo/ncclient/ncclient/  (CISCO 개발자 페이지)

 

NETCONF Python 예제 코드

https://blog.wimwauters.com/networkprogrammability/2020-03-30-netconf_python_part1/

https://blog.wimwauters.com/networkprogrammability/2020-03-31_netconf_python_part2/

 

 

NETCONF Golang 예제 코드

https://github.com/Juniper/go-netconf/tree/master/examples

 

 

NETCONF C++ 예제 코드

https://github.com/CESNET/libnetconf2/tree/master/examples

 

 

RESTCONF Postman 예제 코드

https://blog.wimwauters.com/networkprogrammability/2020-04-02_restconf_introduction_part1/

https://blog.wimwauters.com/networkprogrammability/2020-04-03_restconf_introduction_part2/

 

 

RESTCONF Python 예제  코드

https://blog.wimwauters.com/networkprogrammability/2020-04-04_restconf_python/

 

 

 

 

 

 

 


 

반응형
테스트 및 블로그 작성한 날짜: 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

 

+ Recent posts