L2VPN 패키지 Develop 및 EVPL, EPL 테스트
1. 개요
1.1. 주요 추가 기능
EPL/EVPL 서비스 모델 지원:
EPL (Ethernet Private Line): 포트 기반의 단순한 L2 연결을 지원한다. 사용자는
service-type
을epl
로 지정하여 특정 포트의 모든 트래픽을 원격지로 투명하게 전송할 수 있다.EVPL (Ethernet Virtual Private Line): VLAN 기반의 유연한 L2 연결을 지원한다.
service-type
을evpl
로 지정하여 하나의 물리 포트 위에서 다수의 VLAN을 개별적인 L2VPN 터널로 분리하여 제공할 수 있다.
동적 인터페이스 타입 지원:
NSO 템플릿의
ncs:when
조건문을 활용하여GigabitEthernet
,TenGigE
,HundredGigE
등 다양한 물리 인터페이스 타입을 동적으로 지원한다. 이를 통해 서비스 모델의 수정 없이 다양한 하드웨어 환경에 적용할 수 있다.
VLAN Rewrite 기능 제공:
EVPL 서비스와
dot1q
캡슐화 조합에서 수신(ingress) 트래픽의 VLAN 태그를 조작하는rewrite
기능을 제공한다.translate1to1
: 고객사 VLAN과 서비스 프로바이더의 내부 VLAN을 매핑한다.pop
: 외부 VLAN 태그를 제거하여 Q-in-Q 환경 등에 대응한다.이 기능은 YANG 모델의
rewrite-policy
컨테이너를 통해 조건부로 활성화된다.
1.2. 대상 환경 및 테스트 시나리오
PE (Provider Edge) 라우터:
R1
,R3
(Cisco IOS XR)CE (Customer Edge) 라우터:
CE01
,CE02
L2VPN 연결 구간:
R1
<==>R3
VLAN 연결 정보:
(R1 측) VLAN 10 (
VPC37
) <==> (R3 측) VLAN 110 (VPC40
)(R1 측) VLAN 20 (
VPC38
) <==> (R3 측) VLAN 120 (VPC41
)(R1 측) VLAN 30 (
VPC39
) <==> (R3 측) VLAN 130 (VPC42
)
2. NSO 서비스 패키지 상세 설계
2.1. 서비스 데이터 모델 (l2vpn.yang)
주요 특징:
list l2vpn
: 다수의 L2VPN 서비스 인스턴스를 생성할 수 있도록 리스트 형태로 정의leafref
:device
리프를/ncs:devices/ncs:device/ncs:name
경로에 대한leafref
로 정의하여, NSO에 등록된 장비 중에서만 선택할 수 있도록 하여 입력 오류를 방지when
조건문:vlan-id
나rewrite-policy
와 같이 특정 조건에서만 유효한 파라미터들이 나타나도록when
문을 사용하여 모델의 가독성과 사용자 편의성을 높임enumeration
: 정해진 값(e.g.,epl
,evpl
) 중에서만 선택할 수 있도록 하여 설정의 표준화 강제
전체 코드
module l2vpn { namespace "http://example.com/l2vpn"; prefix l2vpn; import ietf-inet-types { prefix inet; } import tailf-ncs { prefix ncs; } description "L2VPN service using direct interface rewrite for EVPL."; revision 2025-07-29 { description "Final version using direct rewrite for EVPL+dot1q scenario."; } list l2vpn { key name; description "List of L2VPN service instances."; uses ncs:service-data; ncs:servicepoint "l2vpn-servicepoint"; leaf name { type string; description "Unique name for this L2VPN service instance."; } leaf device { type leafref { path "/ncs:devices/ncs:device/ncs:name"; } mandatory true; description "The target device on which to configure the L2VPN."; } container local-l2transport-interface { description "Configuration for the local L2 transport interface."; leaf interface-type { type enumeration { enum "GigabitEthernet"; enum "TenGigE"; enum "HundredGigE"; } mandatory true; description "Type of the L2 transport interface."; } leaf interface-main-id { type string; mandatory true; description "Main part of the interface ID (e.g., '0/0/0/0')."; } leaf subinterface-id { type uint32; mandatory true; description "Subinterface ID, which will be appended after a dot (e.g., 100)."; } leaf mtu { type uint16; default 1522; description "MTU for the L2 transport interface."; } leaf encapsulation-type { type enumeration { enum "dot1q"; enum "untagged"; enum "default"; } mandatory true; description "Encapsulation type for the L2 transport interface."; } leaf vlan-id { when "../encapsulation-type = 'dot1q'"; type uint16 { range "1..4094"; } mandatory true; description "VLAN ID for dot1q encapsulation."; } leaf service-type { type enumeration { enum "epl"; enum "evpl"; } mandatory true; description "Select the L2VPN service type."; } container rewrite-policy { when "../service-type = 'evpl' and ../encapsulation-type = 'dot1q'"; description "Rewrite policy settings for EVPL."; leaf rewrite-type { type enumeration { enum "none"; enum "pop1"; enum "translate1to1"; } mandatory true; description "Type of ingress rewrite operation."; } leaf rewrite-vlan-id { when "../rewrite-type = 'translate1to1'"; type uint16 { range "1..4094"; } mandatory true; description "VLAN ID for 1-to-1 translate rewrite."; } } } container pw-class { description "Pseudowire class configuration for the L2VPN."; leaf pw-class-name { type string { pattern "[a-zA-Z0-9_-]+"; } mandatory true; description "Name of the pseudowire class."; } leaf control-word { type boolean; default "false"; description "Enable or disable control-word for the pseudowire."; } } container xconnect { description "Xconnect group and point-to-point connection configuration."; leaf group-name { type string { pattern "[a-zA-Z0-9_-]+"; } mandatory true; description "Name of the xconnect group (e.g., Provisioned)."; } leaf p2p-name { type string { pattern "[a-zA-Z0-9_-]+"; } mandatory true; description "Name of the point-to-point connection (e.g., epl1)."; } leaf neighbor-ip { type inet:ipv4-address; mandatory true; description "Neighbor IPv4 address for the pseudowire (e.g., 10.10.10.10)."; } leaf pw-id { type uint32; mandatory true; description "Pseudowire ID (e.g., 13)."; } leaf pw-class-ref { type leafref { path "../../pw-class/pw-class-name"; } mandatory true; description "Reference to the pseudowire class to be used for this xconnect."; } } } }
2.2. 서비스 로직 (main.py)
주요 로직:
변수 초기화:
rewrite-type
과 같은 옵션 값의 기본값을 설정조건부 로직:
hasattr
함수를 사용하여 서비스 모델에rewrite-policy
컨테이너가 존재하는지 확인한다. 이를 통해 EPL/EVPL 서비스, 또는rewrite
적용 여부에 따른 분기 처리를 수행문자열 조합:
interface-main-id
와subinterface-id
를 조합하여 실제 장비에 설정될 전체 인터페이스 이름(full_interface_name
)을 동적으로 생성템플릿 적용: 최종적으로 가공된 모든 변수를
tvars
객체에 담아template.apply()
메소드를 호출함으로써 XML 템플릿에 전달
전체 코드
import ncs from ncs.application import Service class ServiceCallbacks(Service): @Service.create def cb_create(self, tctx, root, service, proplist): self.log.info(f"Service create for '{service.name}' using direct rewrite logic.") tvars = ncs.template.Variables() iface = service.local_l2transport_interface # rewrite 관련 변수 초기화 l2_serv_ingress_rewrite_type = 'none' l2_serv_ingress_rewrite_vlan = 0 # rewrite_policy 컨테이너가 있는지 확인 if hasattr(iface, 'rewrite_policy'): rewrite_policy = iface.rewrite_policy l2_serv_ingress_rewrite_type = rewrite_policy.rewrite_type if l2_serv_ingress_rewrite_type == 'translate1to1' and hasattr(rewrite_policy, 'rewrite_vlan_id'): l2_serv_ingress_rewrite_vlan = rewrite_policy.rewrite_vlan_id # 템플릿에 rewrite 변수 전달 tvars.add('L2_SERV_INGRESS_REWRITE_TYPE', l2_serv_ingress_rewrite_type) tvars.add('L2_SERV_INGRESS_REWRITE_VLAN', l2_serv_ingress_rewrite_vlan) # 나머지 모든 변수 전달 interface_id_combined = f"{iface.interface_main_id}.{iface.subinterface_id}" full_interface_name = f"{iface.interface_type}{interface_id_combined}" tvars.add('DEVICE', service.device) tvars.add('INTERFACE_TYPE', iface.interface_type) tvars.add('INTERFACE_ID', interface_id_combined) tvars.add('FULL_INTERFACE_NAME', full_interface_name) tvars.add('MTU', iface.mtu) tvars.add('L2_SERV_ENCAP', iface.encapsulation_type) tvars.add('L2_SERV_VLAN_ID', iface.vlan_id if hasattr(iface, 'vlan_id') else 0) pw = service.pw_class tvars.add('PW_CLASS_NAME', pw.pw_class_name) tvars.add('CONTROL_WORD_ENABLED', 'true' if pw.control_word else 'false') xc = service.xconnect tvars.add('XCONNECT_GROUP_NAME', xc.group_name) tvars.add('P2P_NAME', xc.p2p_name) tvars.add('NEIGHBOR_IP', xc.neighbor_ip) tvars.add('PW_ID', xc.pw_id) tvars.add('PW_CLASS_REF', xc.pw_class_ref) template = ncs.template.Template(service) template.apply('l2vpn-template', tvars) class L2vpn(ncs.application.Application): def setup(self): self.log.info('L2vpn RUNNING') self.register_service('l2vpn-servicepoint', ServiceCallbacks) def teardown(self): self.log.info('L2vpn FINISHED')
2.3. 디바이스 설정 템플릿 (l2vpn-template.xml)
주요 특징:
동적 인터페이스 생성:
ncs:when="{$INTERFACE_TYPE = 'GigabitEthernet'}"
과 같은 조건문을 사용하여 Python에서 전달된 인터페이스 타입에 맞는 XML 블록만 활성화조건부
rewrite
설정:ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE != 'none'}"
조건을 통해rewrite-type
이 'none'이 아닐 경우에만rewrite
설정을 생성하도록 하여 불필요한 설정이 장비에 적용되는 것을 방지NED 호환성: 템플릿의 XML 구조는
cisco-ios-xr
NED의 YANG 모델을 정확히 준수하여 작성되었으며, 이를 통해 NSO는 생성된 XML을 올바른 CLI 명령어로 변환
전체 코드
<config xmlns="http://tail-f.com/ns/config/1.0" xmlns:ncs="http://tail-f.com/ns/ncs"> <devices xmlns="http://tail-f.com/ns/ncs"> <device> <name>{$DEVICE}</name> <config> <interface xmlns="http://tail-f.com/ned/cisco-ios-xr"> <GigabitEthernet-subinterface ncs:when="{$INTERFACE_TYPE = 'GigabitEthernet'}"> <GigabitEthernet> <id>{$INTERFACE_ID}</id> <mode>l2transport</mode> <mtu>{$MTU}</mtu> <encapsulation> <default ncs:when="{$L2_SERV_ENCAP = 'default'}"/> <dot1q ncs:when="{$L2_SERV_ENCAP = 'dot1q'}"> <vlan-id ncs:when="{$L2_SERV_VLAN_ID != 0}">{$L2_SERV_VLAN_ID}</vlan-id> </dot1q> <untagged ncs:when="{$L2_SERV_ENCAP = 'untagged'}"/> </encapsulation> <rewrite ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE != 'none'}"> <ingress> <tag> <pop ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'pop1'}">1</pop> <translate ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1'}">1-to-1</translate> <dot1q ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1' and $L2_SERV_INGRESS_REWRITE_VLAN != 0}">{$L2_SERV_INGRESS_REWRITE_VLAN}</dot1q> <mode>symmetric</mode> </tag> </ingress> </rewrite> </GigabitEthernet> </GigabitEthernet-subinterface> <TenGigE-subinterface ncs:when="{$INTERFACE_TYPE = 'TenGigE'}"> <TenGigE> <id>{$INTERFACE_ID}</id> <mode>l2transport</mode> <mtu>{$MTU}</mtu> <encapsulation> <default ncs:when="{$L2_SERV_ENCAP = 'default'}"/> <dot1q ncs:when="{$L2_SERV_ENCAP = 'dot1q'}"> <vlan-id ncs:when="{$L2_SERV_VLAN_ID != 0}">{$L2_SERV_VLAN_ID}</vlan-id> </dot1q> <untagged ncs:when="{$L2_SERV_ENCAP = 'untagged'}"/> </encapsulation> <rewrite ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE != 'none'}"> <ingress> <tag> <pop ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'pop1'}">1</pop> <translate ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1'}">1-to-1</translate> <dot1q ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1' and $L2_SERV_INGRESS_REWRITE_VLAN != 0}">{$L2_SERV_INGRESS_REWRITE_VLAN}</dot1q> <mode>symmetric</mode> </tag> </ingress> </rewrite> </TenGigE> </TenGigE-subinterface> <HundredGigE-subinterface ncs:when="{$INTERFACE_TYPE = 'HundredGigE'}"> <HundredGigE> <id>{$INTERFACE_ID}</id> <mode>l2transport</mode> <mtu>{$MTU}</mtu> <encapsulation> <default ncs:when="{$L2_SERV_ENCAP = 'default'}"/> <dot1q ncs:when="{$L2_SERV_ENCAP = 'dot1q'}"> <vlan-id ncs:when="{$L2_SERV_VLAN_ID != 0}">{$L2_SERV_VLAN_ID}</vlan-id> </dot1q> <untagged ncs:when="{$L2_SERV_ENCAP = 'untagged'}"/> </encapsulation> <rewrite ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE != 'none'}"> <ingress> <tag> <pop ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'pop1'}">1</pop> <translate ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1'}">1-to-1</translate> <dot1q ncs:when="{$L2_SERV_INGRESS_REWRITE_TYPE = 'translate1to1' and $L2_SERV_INGRESS_REWRITE_VLAN != 0}">{$L2_SERV_INGRESS_REWRITE_VLAN}</dot1q> <mode>symmetric</mode> </tag> </ingress> </rewrite> </HundredGigE> </HundredGigE-subinterface> </interface> <l2vpn xmlns="http://tail-f.com/ned/cisco-ios-xr"> <pw-class> <name>{$PW_CLASS_NAME}</name> <encapsulation> <mpls> <control-word ncs:when="{$CONTROL_WORD_ENABLED = 'true'}"/> </mpls> </encapsulation> </pw-class> <xconnect> <group> <name>{$XCONNECT_GROUP_NAME}</name> <p2p> <name>{$P2P_NAME}</name> <interface> <name>{$FULL_INTERFACE_NAME}</name> </interface> <neighbor> <address>{$NEIGHBOR_IP}</address> <pw-id>{$PW_ID}</pw-id> <ip-version>ipv4</ip-version> <pw-class>{$PW_CLASS_REF}</pw-class> </neighbor> </p2p> </group> </xconnect> </l2vpn> </config> </device> </devices> </config>
4. 구현 및 테스트 시나리오
4.0 토폴로지
4.1. 시나리오 개요
R1
과 R3
PE 라우터 사이에 다수의 EVPL L2VPN을 생성하여, R1 측 VLAN 10, 20, 30을 각각 R3 측 VLAN 110, 120, 130에 연결한다. 각 VLAN 연결은 별개의 L2VPN 서비스 인스턴스로 관리되어 완벽한 트래픽 분리를 보장
4.2. NSO 서비스 생성 명령어
! =================================================== ! 연결 1: VLAN 10 <--> VLAN 110 ! =================================================== l2vpn R1-to-R3_VLAN10 device R1 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 10 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 10 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN10_to_110 xconnect neighbor-ip [R3의 루프백 IP] xconnect pw-id 10110 xconnect pw-class-ref L2VPN-PW-CLASS exit l2vpn R3-to-R1_VLAN110 device R3 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 110 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 110 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN110_to_10 xconnect neighbor-ip [R1의 루프백 IP] xconnect pw-id 10110 xconnect pw-class-ref L2VPN-PW-CLASS exit ! =================================================== ! 연결 2: VLAN 20 <--> VLAN 120 ! =================================================== l2vpn R1-to-R3_VLAN20 device R1 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 20 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 20 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN20_to_120 xconnect neighbor-ip [R3의 루프백 IP] xconnect pw-id 20120 xconnect pw-class-ref L2VPN-PW-CLASS exit l2vpn R3-to-R1_VLAN120 device R3 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 120 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 120 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN120_to_20 xconnect neighbor-ip [R1의 루프백 IP] xconnect pw-id 20120 xconnect pw-class-ref L2VPN-PW-CLASS exit ! =================================================== ! 연결 3: VLAN 30 <--> VLAN 130 ! =================================================== l2vpn R1-to-R3_VLAN30 device R1 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 30 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 30 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN30_to_130 xconnect neighbor-ip [R3의 루프백 IP] xconnect pw-id 30130 xconnect pw-class-ref L2VPN-PW-CLASS exit l2vpn R3-to-R1_VLAN130 device R3 local-l2transport-interface interface-type GigabitEthernet local-l2transport-interface interface-main-id 0/0/0/5 local-l2transport-interface subinterface-id 130 local-l2transport-interface encapsulation-type dot1q local-l2transport-interface vlan-id 130 local-l2transport-interface service-type evpl xconnect group-name L2VPN-GROUP xconnect p2p-name VLAN130_to_30 xconnect neighbor-ip [R1의 루프백 IP] xconnect pw-id 30130 xconnect pw-class-ref L2VPN-PW-CLASS exit commit
4.3. 사전 구성: 스위치, CE 라우터 및 VPC 설정
1. 왼쪽 사이트 (CE01 및 VPC)
VPC IP 주소 설정
VPC37
:ip 192.168.10.4/24 192.168.10.1
VPC38
:ip 192.168.20.5/24 192.168.20.1
VPC39
:ip 192.168.30.6/24 192.168.30.1
스위치 설정
VPC 연결 포트 (Access 모드)
Cisco CLI ! VPC37 연결 포트 interface Ethernet0/1 switchport mode access switchport access vlan 10 ! ! VPC38 연결 포트 interface Ethernet0/2 switchport mode access switchport access vlan 20 ! ! VPC39 연결 포트 interface Ethernet0/3 switchport mode access switchport access vlan 30
CE01 라우터 연결 포트 (Trunk 모드)
interface Ethernet1/0 switchport trunk encapsulation dot1q switchport mode trunk
CE01 라우터 설정
! R1과 연결되는 인터페이스 (물리적 연결) interface Ethernet0/0 no ip address ! ! 스위치와 연결되는 인터페이스 interface Ethernet0/1 no ip address ! ! 각 VLAN을 위한 서브인터페이스 (VPC들의 게이트웨이) interface Ethernet1/0.10 encapsulation dot1q 10 ip address 192.168.10.1 255.255.255.0 ! interface Ethernet1/1.20 encapsulation dot1q 20 ip address 192.168.20.1 255.255.255.0 ! interface Ethernet1/2.30 encapsulation dot1q 30 ip address 192.168.30.1 255.255.255.0
2. 오른쪽 사이트 (CE02 및 VPC)
VPC IP 주소 설정
VPC40
:ip 192.168.10.10/24 192.168.10.2
VPC41
:ip 192.168.20.11/24 192.168.20.2
VPC42
:ip 192.168.30.12/24 192.168.30.2
스위치 설정
VPC 연결 포트 (Access 모드)
! VPC40 연결 포트 interface Ethernet0/1 switchport mode access switchport access vlan 110 ! ! VPC41 연결 포트 interface Ethernet0/2 switchport mode access switchport access vlan 120 ! ! VPC42 연결 포트 interface Ethernet0/3 switchport mode access switchport access vlan 130
CE02 라우터 연결 포트 (Trunk 모드)
interface Ethernet1/0 switchport trunk encapsulation dot1q switchport mode trunk
CE02 라우터 설정
! R3와 연결되는 인터페이스 (물리적 연결) interface Ethernet0/0 no ip address ! ! 스위치와 연결되는 인터페이스 interface Ethernet0/1 no ip address ! ! 각 VLAN을 위한 서브인터페이스 (VPC들의 게이트웨이) interface Ethernet1/0.110 encapsulation dot1q 110 ip address 192.168.10.2 255.255.255.0 ! interface Ethernet1/1.120 encapsulation dot1q 120 ip address 192.168.20.2 255.255.255.0 ! interface Ethernet1/2.130 encapsulation dot1q 130 ip address 192.168.30.2 255.255.255.0