From 94408d1750cf4a0644fd9ddf85e2807847aa1c6e Mon Sep 17 00:00:00 2001 From: Raymond Lam Date: Wed, 23 Jan 2019 21:53:32 +0800 Subject: [PATCH 1/5] Draft for stage 1-4 --- images/utility/requirements.txt | 3 + lessons/lesson-25/jet-idl-17.4R1.16.tar.gz | Bin 0 -> 44983 bytes lessons/lesson-25/stage1/configs/vqfx.txt | 63 +++++++++++ lessons/lesson-25/stage1/guide.md | 38 +++++++ lessons/lesson-25/stage2/configs/vqfx.txt | 78 +++++++++++++ lessons/lesson-25/stage2/guide.md | 87 +++++++++++++++ lessons/lesson-25/stage3/configs/vqfx.txt | 85 ++++++++++++++ lessons/lesson-25/stage3/guide.md | 113 +++++++++++++++++++ lessons/lesson-25/stage4/configs/vqfx.txt | 98 ++++++++++++++++ lessons/lesson-25/stage4/guide.md | 123 +++++++++++++++++++++ lessons/lesson-25/stage5/guide.md | 7 ++ lessons/lesson-25/syringe.yaml | 39 +++++++ 12 files changed, 734 insertions(+) create mode 100644 lessons/lesson-25/jet-idl-17.4R1.16.tar.gz create mode 100644 lessons/lesson-25/stage1/configs/vqfx.txt create mode 100644 lessons/lesson-25/stage1/guide.md create mode 100644 lessons/lesson-25/stage2/configs/vqfx.txt create mode 100644 lessons/lesson-25/stage2/guide.md create mode 100644 lessons/lesson-25/stage3/configs/vqfx.txt create mode 100644 lessons/lesson-25/stage3/guide.md create mode 100644 lessons/lesson-25/stage4/configs/vqfx.txt create mode 100644 lessons/lesson-25/stage4/guide.md create mode 100644 lessons/lesson-25/stage5/guide.md create mode 100644 lessons/lesson-25/syringe.yaml diff --git a/images/utility/requirements.txt b/images/utility/requirements.txt index 8134eb33..445f3e52 100644 --- a/images/utility/requirements.txt +++ b/images/utility/requirements.txt @@ -4,3 +4,6 @@ jsnapy junos-eznc robotframework jinja2 +paho-mqtt +grpcio +grpcio-tools diff --git a/lessons/lesson-25/jet-idl-17.4R1.16.tar.gz b/lessons/lesson-25/jet-idl-17.4R1.16.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1aab2f8a3ef55ae4dc1df49b7f13c9f3e8231bf7 GIT binary patch literal 44983 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 literal 0 HcmV?d00001 diff --git a/lessons/lesson-25/stage1/configs/vqfx.txt b/lessons/lesson-25/stage1/configs/vqfx.txt new file mode 100644 index 00000000..47eedf1b --- /dev/null +++ b/lessons/lesson-25/stage1/configs/vqfx.txt @@ -0,0 +1,63 @@ +version 17.4R1.16; +system { + host-name vqfx; + root-authentication { + encrypted-password "$6$yGARIqqs$.pxKXsYWySQy8BzpcuynxpBzXd7Z2tjqHlCR9wIS/MkEl1NrAsV/ZwNrJCtCrWQOovlmEgPvWXRzcPAykvCY8/"; ## SECRET-DATA + } + login { + user antidote { + uid 2000; + class super-user; + authentication { + encrypted-password "$6$L1CNVBaS$lxgFvGaqn5ixBoCNfNod44gQ26.3IYI.hurq/pL0a9Gqq8837HfP0iMZ7SKpzTF34wNmbK4axz4EIijAtLHRl0"; ## SECRET-DATA + } + } + password { + change-type set-transitions; + minimum-changes 0; + } + } + services { + ssh { + root-login allow; + } + netconf { + ssh; + rfc-compliant; + } + rest { + http { + port 8080; + } + enable-explorer; + } + } + syslog { + user * { + any emergency; + } + file messages { + any notice; + authorization info; + } + file interactive-commands { + interactive-commands any; + } + } +} +interfaces { + em0 { + unit 0 { + family inet { + address 10.0.0.15/24; + } + } + } + em1 { + unit 0 { + family inet { + address 169.254.0.2/24; + } + } + } +} diff --git a/lessons/lesson-25/stage1/guide.md b/lessons/lesson-25/stage1/guide.md new file mode 100644 index 00000000..73a81a51 --- /dev/null +++ b/lessons/lesson-25/stage1/guide.md @@ -0,0 +1,38 @@ +## Automating with JET + +**Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** + +--- + +### Chapter 1 - JET rpc/notification configuration + +[Placeholder for JET introduction] + +[Placeholder for describing required procedures to enable rpc/notification services] + +Apply below configuration to enable notification and GRPC request-response service on vQFX. + +_Shall we explain the hidden command clear-text here?_ + +``` +configure +set system services extension-service notification port 1883 allow-clients address 0.0.0.0/0 +set system services extension-service request-response grpc clear-text port 32767 +commit and-quit +``` + + +_Seems there is no operational commands to verify the rpc/notification status, so just verify the listening port_ + +We can check the listening port to verify the rpc and notification service are enabled. + +``` +show system connections | match LISTEN | match "\.1883|\.32767" +``` + + +> Expected output (remove later) +> +> tcp4 0 0 *.1883 *.* LISTEN +> tcp46 0 0 *.32767 *.* LISTEN + diff --git a/lessons/lesson-25/stage2/configs/vqfx.txt b/lessons/lesson-25/stage2/configs/vqfx.txt new file mode 100644 index 00000000..212af430 --- /dev/null +++ b/lessons/lesson-25/stage2/configs/vqfx.txt @@ -0,0 +1,78 @@ +version 17.4R1.16; +system { + host-name vqfx; + root-authentication { + encrypted-password "$6$yGARIqqs$.pxKXsYWySQy8BzpcuynxpBzXd7Z2tjqHlCR9wIS/MkEl1NrAsV/ZwNrJCtCrWQOovlmEgPvWXRzcPAykvCY8/"; ## SECRET-DATA + } + login { + user antidote { + uid 2000; + class super-user; + authentication { + encrypted-password "$6$L1CNVBaS$lxgFvGaqn5ixBoCNfNod44gQ26.3IYI.hurq/pL0a9Gqq8837HfP0iMZ7SKpzTF34wNmbK4axz4EIijAtLHRl0"; ## SECRET-DATA + } + } + password { + change-type set-transitions; + minimum-changes 0; + } + } + services { + ssh { + root-login allow; + } + extension-service { + request-response { + grpc { + clear-text { + port 32767; + } + } + } + notification { + port 1883; + allow-clients { + address 0.0.0.0/0; + } + } + } + netconf { + ssh; + rfc-compliant; + } + rest { + http { + port 8080; + } + enable-explorer; + } + } + syslog { + user * { + any emergency; + } + file messages { + any notice; + authorization info; + } + file interactive-commands { + interactive-commands any; + } + } +} +interfaces { + em0 { + unit 0 { + family inet { + address 10.0.0.15/24; + } + } + } + em1 { + unit 0 { + family inet { + address 169.254.0.2/24; + } + } + } +} diff --git a/lessons/lesson-25/stage2/guide.md b/lessons/lesson-25/stage2/guide.md new file mode 100644 index 00000000..981fdefe --- /dev/null +++ b/lessons/lesson-25/stage2/guide.md @@ -0,0 +1,87 @@ +## Automating with JET + +**Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** + +--- + +### Chapter 2 - Receive events from notification service using MQTT client + +[Placeholder to explain how to subscribe JET notification events] + +[JET Notification API document](https://www.juniper.net/documentation/en_US/jet17.4/topics/concept/jet-notification-api-overview.html) + +We're going to use Python MQTT client to subscribe the JET notification events. + +First, go to the Python interactive prompt and import the MQTT module. + +``` +python +import paho.mqtt.client as mqtt +``` + + +After that, create a MQTT client, define the `on_connect` callback function to subscribe the event. Here, we will subscribe `/junos/events/kernel/interfaces/ifa/add` topic to get notification if any interface address is added. + +``` +client = mqtt.Client() + +def on_connect(client, userdata, flags, rc): + client.subscribe("/junos/events/kernel/interfaces/ifa/add/#") + +client.on_connect = on_connect +``` + + +Next, define `on_message` callback function to print the notification message payload. + +``` +def on_message(client, userdata, msg): + print("%s %s" % (msg.topic, msg.payload)) + +client.on_message = on_message +``` + + +Finally, connect to vQFX tcp 1883 we configured in previous stage, and then call the `loop_forever()` function to wait for events. + +``` +client.connect('vqfx', 1883, 60) +client.loop_forever() +``` + + +Now, create an new interface on vQFX. + +``` +configure +set interfaces xe-0/0/0 unit 0 family inet address 192.168.10.1/24 +commit and-quit +``` + + +Change to `linux` terminal, you should be able to see the `KERNEL_EVENT_IFA_ADD` event. + +Press `Ctrl-C` to exit the loop. + +> Expected output (remove later) +> +> /junos/events/kernel/interfaces/ifa/add/xe-0/0/1.0/inet/192.168.20.1/32 { +> "jet-event": { +> "event-id": "KERNEL_EVENT_IFA_ADD", +> "hostname": "vqfx", +> "time": "2019-01-22-14:59:06", +> "severity": "INFO", +> "facility": "KERNEL", +> "attributes": { +> "name": "xe-0/0/0", +> "subunit": 0, +> "family": "inet", +> "local-address": "192.168.10.1/32", +> "destination-address": "192.168.10.0/24", +> "broadcast-address": "192.168.10.255/32", +> "generation-number": 144, +> "flags": 192 +> } +> } +> } + diff --git a/lessons/lesson-25/stage3/configs/vqfx.txt b/lessons/lesson-25/stage3/configs/vqfx.txt new file mode 100644 index 00000000..b382474a --- /dev/null +++ b/lessons/lesson-25/stage3/configs/vqfx.txt @@ -0,0 +1,85 @@ +version 17.4R1.16; +system { + host-name vqfx; + root-authentication { + encrypted-password "$6$yGARIqqs$.pxKXsYWySQy8BzpcuynxpBzXd7Z2tjqHlCR9wIS/MkEl1NrAsV/ZwNrJCtCrWQOovlmEgPvWXRzcPAykvCY8/"; ## SECRET-DATA + } + login { + user antidote { + uid 2000; + class super-user; + authentication { + encrypted-password "$6$L1CNVBaS$lxgFvGaqn5ixBoCNfNod44gQ26.3IYI.hurq/pL0a9Gqq8837HfP0iMZ7SKpzTF34wNmbK4axz4EIijAtLHRl0"; ## SECRET-DATA + } + } + password { + change-type set-transitions; + minimum-changes 0; + } + } + services { + ssh { + root-login allow; + } + extension-service { + request-response { + grpc { + clear-text { + port 32767; + } + } + } + notification { + port 1883; + allow-clients { + address 0.0.0.0/0; + } + } + } + netconf { + ssh; + rfc-compliant; + } + rest { + http { + port 8080; + } + enable-explorer; + } + } + syslog { + user * { + any emergency; + } + file messages { + any notice; + authorization info; + } + file interactive-commands { + interactive-commands any; + } + } +} +interfaces { + xe-0/0/0 { + unit 0 { + family inet { + address 192.168.10.1/24; + } + } + } + em0 { + unit 0 { + family inet { + address 10.0.0.15/24; + } + } + } + em1 { + unit 0 { + family inet { + address 169.254.0.2/24; + } + } + } +} diff --git a/lessons/lesson-25/stage3/guide.md b/lessons/lesson-25/stage3/guide.md new file mode 100644 index 00000000..04d706f5 --- /dev/null +++ b/lessons/lesson-25/stage3/guide.md @@ -0,0 +1,113 @@ +## Automating with JET + +**Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** + +--- + +### Chapter 3 - IDL compilation and basic GRPC JET testing + +[Placeholder to explain JET GPRC request-response service and what is IDL] + +[Compiling the IDL file](https://www.juniper.net/documentation/en_US/jet1.0/topics/task/jet-complie-idl-using-thrift.html) + +[Quick Start guide for GRPC in Python](https://grpc.io/docs/quickstart/python.html) + +We will use the protoc compiler to compile the IDL file. You can download the IDL file from [Juniper support website](https://support.juniper.net/support/downloads/?p=jet). + +In this lesson, the IDL file is pre-downloaded and now let's unarchive it. + +``` +cd /antidote/lessons/lesson-25 +tar -xzf jet-idl-17.4R1.16.tar.gz +``` + + +Next, compile the protocol buffer definition. + +``` +python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. ./proto/*.proto +``` + + +Verify the compiled python class. + +``` +ls -l *pb2* +``` + + +Now, we can test the JET GRPC by authenticating to the vQFX. + +First, go to Python interactive prompt and import the required modules. + +``` +python +import grpc +import jnx_addr_pb2 as addr +import prpd_common_pb2 as prpd +import authentication_service_pb2 as auth +import authentication_service_pb2_grpc +import rib_service_pb2 as rib +import rib_service_pb2_grpc +``` + + +Call the JET authentication API to connect to the vQFX through GRPC channel. + +``` +channel = grpc.insecure_channel('vqfx:32767') + +auth_stub = authentication_service_pb2_grpc.LoginStub(channel) +response = auth_stub.LoginCheck( + auth.LoginRequest( + user_name='antidote', + password='antidotepassword', + client_id='jet', + ) +) +``` + + +Print the response to verify the connection is established. + +``` +print(response.result) +``` + + +Now, try to call the Route Add API to insert a static route dynamically. + +``` +rib_stub = rib_service_pb2_grpc.RibStub(channel) +result = rib_stub.RouteAdd( + rib.RouteUpdateRequest( + routes=[rib.RouteEntry( + key=rib.RouteMatchFields( + dest_prefix=prpd.NetworkAddress(inet=addr.IpAddress(addr_string='192.168.20.0')), + dest_prefix_len=24, + table=prpd.RouteTable(rtt_name=prpd.RouteTableName(name='inet.0')), + ), + nexthop=rib.RouteNexthop( + gateways=[rib.RouteGateway( + gateway_address=prpd.NetworkAddress(inet=addr.IpAddress(addr_string='192.168.10.2')) + )] + ) + )] + ) +) +``` + + +Print the response to verify the route add status. +``` +print(result) +``` + + +After that, try to show route in vQFX to confirm the route is being added. + +``` +show route table inet.0 +``` + + diff --git a/lessons/lesson-25/stage4/configs/vqfx.txt b/lessons/lesson-25/stage4/configs/vqfx.txt new file mode 100644 index 00000000..a867ac9b --- /dev/null +++ b/lessons/lesson-25/stage4/configs/vqfx.txt @@ -0,0 +1,98 @@ +version 17.4R1.16; +system { + host-name vqfx; + root-authentication { + encrypted-password "$6$yGARIqqs$.pxKXsYWySQy8BzpcuynxpBzXd7Z2tjqHlCR9wIS/MkEl1NrAsV/ZwNrJCtCrWQOovlmEgPvWXRzcPAykvCY8/"; ## SECRET-DATA + } + login { + user antidote { + uid 2000; + class super-user; + authentication { + encrypted-password "$6$L1CNVBaS$lxgFvGaqn5ixBoCNfNod44gQ26.3IYI.hurq/pL0a9Gqq8837HfP0iMZ7SKpzTF34wNmbK4axz4EIijAtLHRl0"; ## SECRET-DATA + } + } + password { + change-type set-transitions; + minimum-changes 0; + } + } + services { + ssh { + root-login allow; + } + extension-service { + request-response { + grpc { + clear-text { + port 32767; + } + } + } + notification { + port 1883; + allow-clients { + address 0.0.0.0/0; + } + } + } + netconf { + ssh; + rfc-compliant; + } + rest { + http { + port 8080; + } + enable-explorer; + } + } + syslog { + user * { + any emergency; + } + file messages { + any notice; + authorization info; + } + file interactive-commands { + interactive-commands any; + } + } +} +interfaces { + xe-0/0/0 { + unit 0 { + family inet { + address 192.168.10.1/24; + } + } + } + xe-0/0/1 { + unit 0 { + family inet { + address 192.168.10.2/24; + } + } + } + em0 { + unit 0 { + family inet { + address 10.0.0.15/24; + } + } + } + em1 { + unit 0 { + family inet { + address 169.254.0.2/24; + } + } + } +} +routing-instances { + VR { + instance-type virtual-router; + interface xe-0/0/1.0; + } +} diff --git a/lessons/lesson-25/stage4/guide.md b/lessons/lesson-25/stage4/guide.md new file mode 100644 index 00000000..061956f0 --- /dev/null +++ b/lessons/lesson-25/stage4/guide.md @@ -0,0 +1,123 @@ +## Automating with JET + +**Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** + +--- + +### Chapter 4 - JET Firewall API to create/update firewall filter + +In this stage, to demonstrate additional JET API capability, we will use JET firewall API to insert a new firewall filter to vQFX. + +First, we repeat what we have done in previous stage - compile the IDL package, go to Python interactive prompt, import the JET GPRC module, and then login to the vQFX. + +``` +cd /antidote/lessons/lesson-25 +tar -xzf jet-idl-17.4R1.16.tar.gz +python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. ./proto/*.proto + +python +import grpc +import jnx_addr_pb2 as addr +import authentication_service_pb2 as auth +import authentication_service_pb2_grpc +import firewall_service_pb2 as fw +import firewall_service_pb2_grpc + +channel = grpc.insecure_channel('vqfx:32767') +auth_stub = authentication_service_pb2_grpc.LoginStub(channel) +response = auth_stub.LoginCheck( + auth.LoginRequest( + user_name='antidote', + password='antidotepassword', + client_id='jet', + ) +) +``` + + +A new routing instance is created in the vQFX to simulate a neighboring device. Interface xe-0/0/1 and xe-0/0/2 is bridged together, so on vQFX we can ping to 192.168.10.2, which is the IP address in the routing instance "VR". Let's verify it. + +``` +ping 192.168.10.2 count 3 +``` + + +Now, we use the JET firewall API to create a new firewall filter call "filter-by-jet". The filter contains two terms, the first is to log and permit ICMP traffic, and the last explicit term is to log and discard all traffic. + +``` +fw_stub = firewall_service_pb2_grpc.AclServiceStub(channel) + +filter = fw.AccessList( + acl_name='filter-by-jet', + acl_type=fw.ACL_TYPE_CLASSIC, + acl_family=fw.ACL_FAMILY_INET, + acl_flag=fw.ACL_FLAGS_NONE, + ace_list=[ + fw.AclEntry(inet_entry=fw.AclInetEntry( + ace_name='t1', + ace_op=fw.ACL_ENTRY_OPERATION_ADD, + adjacency=fw.AclAdjacency(type=fw.ACL_ADJACENCY_AFTER), + matches=fw.AclEntryMatchInet(match_protocols=[fw.AclMatchProtocol(min=1, max=1, match_op=fw.ACL_MATCH_OP_EQUAL)]), + actions=fw.AclEntryInetAction( + action_t=fw.AclEntryInetTerminatingAction(action_accept=1), + actions_nt=fw.AclEntryInetNonTerminatingAction(action_log=1) + ) + )), + fw.AclEntry(inet_entry=fw.AclInetEntry( + ace_name='t2', + ace_op=fw.ACL_ENTRY_OPERATION_ADD, + adjacency=fw.AclAdjacency(type=fw.ACL_ADJACENCY_AFTER), + actions=fw.AclEntryInetAction( + action_t=fw.AclEntryInetTerminatingAction(action_discard=1), + actions_nt=fw.AclEntryInetNonTerminatingAction(action_log=1) + ), + )) + ] +) +result = fw_stub.AccessListAdd(filter) +``` + + +We can verify the firewall filter is actually added to the data plane by using the PFE command. + +``` +request pfe execute target fpc0 timeout 0 command "show firewall" | no-more +``` + + +Next, apply the firewall filter to interface xe-0/0/0. + +``` +result = fw_stub.AccessListBindAdd( + fw.AccessListObjBind( + acl=filter, + obj_type=fw.ACL_BIND_OBJ_TYPE_INTERFACE, + bind_object=fw.AccessListBindObjPoint(intf='xe-0/0/0.0'), + bind_direction=fw.ACL_BIND_DIRECTION_INPUT, + bind_family=fw.ACL_FAMILY_INET + ) +) +``` + + +To verify, it, we can ping 192.168.10.2 again and then check the firewall log. + +``` +ping 192.168.10.2 count 3 +show firewall log +``` + + +And then try to SSH to 192.168.10.2, it should fail. + +``` +ssh 192.168.10.2 +``` + + +Press `Ctrl-C` now to stop the SSH, and the check the firewall log again. + +``` +show firewall log +``` + diff --git a/lessons/lesson-25/stage5/guide.md b/lessons/lesson-25/stage5/guide.md new file mode 100644 index 00000000..d8d89605 --- /dev/null +++ b/lessons/lesson-25/stage5/guide.md @@ -0,0 +1,7 @@ +## Automating with JET + +**Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** + +--- + +### Chapter 5 - Closed loop automation with JET diff --git a/lessons/lesson-25/syringe.yaml b/lessons/lesson-25/syringe.yaml new file mode 100644 index 00000000..794238a4 --- /dev/null +++ b/lessons/lesson-25/syringe.yaml @@ -0,0 +1,39 @@ +--- +lessonName: Automating with Junos Extension Toolkit (JET) +lessonId: 25 +category: configuration +tier: local +juniperSpecific: true + +utilities: +- name: linux + image: raylam/utility + +devices: +- name: vqfx + image: raylam/vqfx-full-17.4:190123 + ports: + - 830 + - 1883 + - 32767 + +connections: +- a: vqfx + b: linux + subnet: 10.1.0.0/24 + +stages: + - id: 1 + description: JET rpc/notification configuration + + - id: 2 + description: Receive events from notification service using MQTT client + + - id: 3 + description: IDL compilation and basic GRPC JET testing + + - id: 4 + description: JET Firewall API to create/update firewall filter + + - id: 5 + description: Closed loop automation with JET From ec3d88c629e205c29bc3455d92cfda29507d57d9 Mon Sep 17 00:00:00 2001 From: Tony Chan Date: Fri, 25 Jan 2019 21:15:21 +1100 Subject: [PATCH 2/5] Updated stage 1-4 --- lessons/lesson-25/stage1/guide.md | 34 +++++++++---- lessons/lesson-25/stage2/guide.md | 83 ++++++++++++++++++++----------- lessons/lesson-25/stage3/guide.md | 38 ++++++++++---- lessons/lesson-25/stage4/guide.md | 23 ++++++--- 4 files changed, 121 insertions(+), 57 deletions(-) diff --git a/lessons/lesson-25/stage1/guide.md b/lessons/lesson-25/stage1/guide.md index 73a81a51..34c91324 100644 --- a/lessons/lesson-25/stage1/guide.md +++ b/lessons/lesson-25/stage1/guide.md @@ -6,13 +6,30 @@ ### Chapter 1 - JET rpc/notification configuration -[Placeholder for JET introduction] +#### What is JET? +Juniper Extension Toolkit (JET) is a framework that exposes API functionality made available by the internal Junos OS daemons. Each internal daemon exposes its own APIs. All of the APIs are accessible using the gRPC framework for remote procedure calls (RPCs). -[Placeholder for describing required procedures to enable rpc/notification services] +JET supports the following: +* Multiple languages for applications that run off-box +* Python for applications that run on a device running Junos OS +* Applications written in C to run on devices that do not use the JET APIs +* An event notification method that enables the applications to respond to selected system events -Apply below configuration to enable notification and GRPC request-response service on vQFX. +There are two types of services JET provides: +* Request-response - An application can issue a request and wait for the response from Junos OS. (RPC model, GRPC based) +* Notification - An application can receive asynchronous notification of events happening on Junos OS. (publish-subscribe model. MQTT based) + +For more informations about the JET can be found [here](https://www.juniper.net/documentation/en_US/jet18.4/topics/concept/jet-architecture.html). + +In this lab we are going to explore a off-box python JET applications. + +#### Config the Junos device for JET +First of all, to run an off-box JET application, we need to enable the request-response configuration on the Junos OS device. + +The gRPC can be run in clear-text mode _(insecure! that's why it is hidden and for lab test only +!)_ or SSL encrypted mode for enhanced security. For simpilicity we'll go with clear-text in the lab. More information about gRPC over SSL can be found [here](https://www.juniper.net/documentation/en_US/jet18.4/topics/topic-map/jet-off-box-apps.html). -_Shall we explain the hidden command clear-text here?_ +Apply below configuration to enable notification and GRPC request-response service on vQFX. ``` configure @@ -22,8 +39,6 @@ commit and-quit ``` -_Seems there is no operational commands to verify the rpc/notification status, so just verify the listening port_ - We can check the listening port to verify the rpc and notification service are enabled. ``` @@ -31,8 +46,9 @@ show system connections | match LISTEN | match "\.1883|\.32767" ``` -> Expected output (remove later) -> +Expected output +``` > tcp4 0 0 *.1883 *.* LISTEN > tcp46 0 0 *.32767 *.* LISTEN - +``` +Now the Junos OS Device is ready for off-box JET applications and it's time to get some action! In the next chapter, we'll go through the notifiaction mechanism and collect some events from the MQTT event bus. diff --git a/lessons/lesson-25/stage2/guide.md b/lessons/lesson-25/stage2/guide.md index 981fdefe..db14b8a3 100644 --- a/lessons/lesson-25/stage2/guide.md +++ b/lessons/lesson-25/stage2/guide.md @@ -5,14 +5,27 @@ --- ### Chapter 2 - Receive events from notification service using MQTT client +#### Message Broker and PUB-SUB model +Before we deep dive into notification service, let's get an introduction on message broker and publish-subscribe (PUB-SUB) model. -[Placeholder to explain how to subscribe JET notification events] +![JET System Archtecture](https://www.juniper.net/documentation/images/g043543.png) -[JET Notification API document](https://www.juniper.net/documentation/en_US/jet17.4/topics/concept/jet-notification-api-overview.html) -We're going to use Python MQTT client to subscribe the JET notification events. +A message broker system is hub and spoke based model where +- Broker accepts messages from clients and delivers to other interested clients +- Client either publish an message with a topic, or subscribe to a topic or both +- Topic is a namespace on the broker. Client subscribes or publish to a topics +- Publish: a Client sending a message to a Broker with a topic +- Subscribe: a Client requesting broker what topic it is interested.The broker will deliver messages to this client based on the topic it subscribed. -First, go to the Python interactive prompt and import the MQTT module. +#### JET Notification Service +Juniper JET notification service is a message broker system based on [MQTT](http://mqtt.org/) protocol to deliver system events. Junos system daemons such as RPD will generate messages and publish them to the JET message broker thru eventd with specific topics. For example, interface events (link up/down) will have topic ```/junos/events/kernel/interfaces``` while route table related event will have topic ```/junos/events/kernel/route-table```. The list of topic available can be found [here](https://www.juniper.net/documentation/en_US/jet17.4/topics/concept/jet-notification-api-overview.html). + + +#### Creating a Python MQTT Client +We're going to create a Python MQTT client to collect JET notification events. + +First of all, go to the Python interactive prompt and import the MQTT module. ``` python @@ -20,7 +33,8 @@ import paho.mqtt.client as mqtt ``` -After that, create a MQTT client, define the `on_connect` callback function to subscribe the event. Here, we will subscribe `/junos/events/kernel/interfaces/ifa/add` topic to get notification if any interface address is added. +After that, create a MQTT client object. Then define the `on_connect` callback function, which is triggered once the client connects to the JET MQTT broker, to subscribe the event. Here, we will subscribe to the topic `/junos/events/kernel/interfaces/ifa/add` which includes all new interfaces adress events. +At last we bind the on_connect function to the client object. ``` client = mqtt.Client() @@ -32,7 +46,7 @@ client.on_connect = on_connect ``` -Next, define `on_message` callback function to print the notification message payload. +Next, define `on_message` callback function, which is triggered whatever a message is received, to print the notification message payload. Then we bind the on_message function to the client object. ``` def on_message(client, userdata, msg): @@ -42,7 +56,7 @@ client.on_message = on_message ``` -Finally, connect to vQFX tcp 1883 we configured in previous stage, and then call the `loop_forever()` function to wait for events. +Finally, instruct the client to connect to vQFX tcp 1883 we configured in previous stage, and start the main event loop function `loop_forever()` to wait for events. ``` client.connect('vqfx', 1883, 60) @@ -50,7 +64,12 @@ client.loop_forever() ``` -Now, create an new interface on vQFX. +The client is now ready to received JET notification event. + + +#### Triggering a event + +Now, we try to trigger a new interface address event by create an new interface on vQFX. ``` configure @@ -59,29 +78,33 @@ commit and-quit ``` -Change to `linux` terminal, you should be able to see the `KERNEL_EVENT_IFA_ADD` event. +Once the commit is completed, Eventd will receive the new IFA event and deliver it all clients who subscribed the IFA topic. + +Now change to `linux` terminal, you should be able to see the `KERNEL_EVENT_IFA_ADD` event. Press `Ctrl-C` to exit the loop. -> Expected output (remove later) -> -> /junos/events/kernel/interfaces/ifa/add/xe-0/0/1.0/inet/192.168.20.1/32 { -> "jet-event": { -> "event-id": "KERNEL_EVENT_IFA_ADD", -> "hostname": "vqfx", -> "time": "2019-01-22-14:59:06", -> "severity": "INFO", -> "facility": "KERNEL", -> "attributes": { -> "name": "xe-0/0/0", -> "subunit": 0, -> "family": "inet", -> "local-address": "192.168.10.1/32", -> "destination-address": "192.168.10.0/24", -> "broadcast-address": "192.168.10.255/32", -> "generation-number": 144, -> "flags": 192 -> } -> } -> } +Here's a sample output of the message: +``` + /junos/events/kernel/interfaces/ifa/add/xe-0/0/1.0/inet/192.168.20.1/32 { + "jet-event": { + "event-id": "KERNEL_EVENT_IFA_ADD", + "hostname": "vqfx", + "time": "2019-01-22-14:59:06", + "severity": "INFO", + "facility": "KERNEL", + "attributes": { + "name": "xe-0/0/0", + "subunit": 0, + "family": "inet", + "local-address": "192.168.10.1/32", + "destination-address": "192.168.10.0/24", + "broadcast-address": "192.168.10.255/32", + "generation-number": 144, + "flags": 192 + } + } + } +``` +In the next chapter we are going to explore JET request-response calls. diff --git a/lessons/lesson-25/stage3/guide.md b/lessons/lesson-25/stage3/guide.md index 04d706f5..d6358783 100644 --- a/lessons/lesson-25/stage3/guide.md +++ b/lessons/lesson-25/stage3/guide.md @@ -4,17 +4,25 @@ --- -### Chapter 3 - IDL compilation and basic GRPC JET testing +### Chapter 3 - GRPC, IDL compilation and basic GRPC JET testing -[Placeholder to explain JET GPRC request-response service and what is IDL] +#### Junos JET, gRPC and IDL -[Compiling the IDL file](https://www.juniper.net/documentation/en_US/jet1.0/topics/task/jet-complie-idl-using-thrift.html) +Juniper JET uses [gRPC](http://www.grpc.io/), a remote procedure call (RPC) framework, for cross-language services as a mechanism to enable request-response service. gRPC provides an interface definition language (IDL) that enables you to define APIs. These IDL files (with .proto as the file extension) are compiled using the protoc compiler to generate source code to be used for the server and client applications. The gRPC server is part of the JET service process (jsd), which runs on Junos OS. -[Quick Start guide for GRPC in Python](https://grpc.io/docs/quickstart/python.html) +![JET System Archtecture](https://www.juniper.net/documentation/images/g043543.png) + + +#### Compiling Junos IDL into python library +Junos IDL file can be downloaded from Juniper support webpage. For details on downloading and compiling the IDL please refer [here](https://www.juniper.net/documentation/en_US/jet1.0/topics/task/jet-complie-idl-using-thrift.html) + +For those who wants more details about gRPC in Python, there's a [quick start guide](https://grpc.io/docs/quickstart/python.html) grom grpc.io. We will use the protoc compiler to compile the IDL file. You can download the IDL file from [Juniper support website](https://support.juniper.net/support/downloads/?p=jet). -In this lesson, the IDL file is pre-downloaded and now let's unarchive it. +It's time to start the lab! + +To save time, the IDL file is pre-downloaded already alrady. First we need to unarchive it: ``` cd /antidote/lessons/lesson-25 @@ -22,24 +30,31 @@ tar -xzf jet-idl-17.4R1.16.tar.gz ``` -Next, compile the protocol buffer definition. +Next, compile the protocol buffer into python stub code: ``` python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. ./proto/*.proto ``` -Verify the compiled python class. +Verify the python stub class are generated: ``` ls -l *pb2* ``` -Now, we can test the JET GRPC by authenticating to the vQFX. +The python stub class is now ready, the next step we will use it to perform gRPC to the Junos device. + +#### Python gRPC Client +In this exercise we are going to create a simple python gRPC client to manipulate the Junos routing table. + First, go to Python interactive prompt and import the required modules. +`authentication_service_pb2` for the Junos Authentication +`rib_service_pb2` is the Junos JET RIB gRPC service. + ``` python import grpc @@ -52,7 +67,7 @@ import rib_service_pb2_grpc ``` -Call the JET authentication API to connect to the vQFX through GRPC channel. +Now we create a gRPC tcp connection to the vQFX and perform the authentication. ``` channel = grpc.insecure_channel('vqfx:32767') @@ -75,7 +90,7 @@ print(response.result) ``` -Now, try to call the Route Add API to insert a static route dynamically. +Now, we can call the Route Add API to insert a static route dynamically. ``` rib_stub = rib_service_pb2_grpc.RibStub(channel) @@ -104,10 +119,11 @@ print(result) ``` -After that, try to show route in vQFX to confirm the route is being added. +At last, verify the route is being added by doing CLI command `show route` on the vQFX. ``` show route table inet.0 ``` +Easy isn't it? With Juniper JET you can dynamically manage device configuration and runtime status such as route tables, ACL , interface status, etc without touching the CLI at all. This API interfaces open a whole new world for network applications. For more information about the API, please refer to the [JET Service APIs guide](https://www.juniper.net/documentation/en_US/jet18.2/topics/concept/jet-service-apis-overview.html) diff --git a/lessons/lesson-25/stage4/guide.md b/lessons/lesson-25/stage4/guide.md index 061956f0..bb1a6f60 100644 --- a/lessons/lesson-25/stage4/guide.md +++ b/lessons/lesson-25/stage4/guide.md @@ -6,9 +6,11 @@ ### Chapter 4 - JET Firewall API to create/update firewall filter -In this stage, to demonstrate additional JET API capability, we will use JET firewall API to insert a new firewall filter to vQFX. +In this stage, we demonstrate additional JET API capability by using JET firewall API to insert a new firewall filter to vQFX. -First, we repeat what we have done in previous stage - compile the IDL package, go to Python interactive prompt, import the JET GPRC module, and then login to the vQFX. + +#### Preperation +Firstly, we repeat what we have done in previous stage - compile the IDL package, go to Python interactive prompt, import the JET GPRC module, and then login to the vQFX. ``` cd /antidote/lessons/lesson-25 @@ -35,14 +37,15 @@ response = auth_stub.LoginCheck( ``` -A new routing instance is created in the vQFX to simulate a neighboring device. Interface xe-0/0/1 and xe-0/0/2 is bridged together, so on vQFX we can ping to 192.168.10.2, which is the IP address in the routing instance "VR". Let's verify it. +We simulate a neighboring device by creating a new routing instance in the vQFX. Interface xe-0/0/1 and xe-0/0/2 is connected together, so on vQFX we can ping from the master routing instance to 192.168.10.2, which is the IP address in the routing instance "VR". Let's try to verify it: ``` ping 192.168.10.2 count 3 ``` -Now, we use the JET firewall API to create a new firewall filter call "filter-by-jet". The filter contains two terms, the first is to log and permit ICMP traffic, and the last explicit term is to log and discard all traffic. +#### Create ACL via JET +Now, we use the JET firewall API to create a new firewall filter call "filter-by-jet". This filter contains two terms, the first one is to log and permit ICMP traffic, and the last one is to log and discard all traffic. ``` fw_stub = firewall_service_pb2_grpc.AclServiceStub(channel) @@ -85,7 +88,8 @@ request pfe execute target fpc0 timeout 0 command "show firewall" | no-more ``` -Next, apply the firewall filter to interface xe-0/0/0. +#### Apply ACL to interface via JET +Now, we apply the firewall filter to interface xe-0/0/0. ``` result = fw_stub.AccessListBindAdd( @@ -100,7 +104,10 @@ result = fw_stub.AccessListBindAdd( ``` -To verify, it, we can ping 192.168.10.2 again and then check the firewall log. + +#### Verify the ACL + +To verify the firewall ACL is being applied, we ping 192.168.10.2 again and then check the firewall log. ``` ping 192.168.10.2 count 3 @@ -108,7 +115,7 @@ show firewall log ``` -And then try to SSH to 192.168.10.2, it should fail. +Then try to SSH to 192.168.10.2, it should fail. ``` ssh 192.168.10.2 @@ -121,3 +128,5 @@ Press `Ctrl-C` now to stop the SSH, and the check the firewall log again. show firewall log ``` + +This concludes ourJunos JET gRPC demostration. In the next lesson are we going explore closed loop automation by employing both JET Notification Service and JET RPC. From eda435da2fb2bf18b4ad4814a0a83a6f1e96e68a Mon Sep 17 00:00:00 2001 From: Raymond Lam Date: Sun, 3 Feb 2019 11:47:46 +0800 Subject: [PATCH 3/5] Minor update and draft for stage 5 --- lessons/lesson-25/add_firewall_filter.py | 57 +++++++++++ lessons/lesson-25/stage1/configs/vqfx.txt | 4 +- lessons/lesson-25/stage1/guide.md | 20 ++-- lessons/lesson-25/stage2/configs/vqfx.txt | 4 +- lessons/lesson-25/stage2/guide.md | 33 +------ lessons/lesson-25/stage3/configs/vqfx.txt | 4 +- lessons/lesson-25/stage3/guide.md | 22 ++--- lessons/lesson-25/stage4/configs/vqfx.txt | 4 +- lessons/lesson-25/stage4/guide.md | 6 +- lessons/lesson-25/stage5/configs/vqfx.txt | 109 ++++++++++++++++++++++ lessons/lesson-25/stage5/guide.md | 83 ++++++++++++++++ 11 files changed, 281 insertions(+), 65 deletions(-) create mode 100644 lessons/lesson-25/add_firewall_filter.py create mode 100644 lessons/lesson-25/stage5/configs/vqfx.txt diff --git a/lessons/lesson-25/add_firewall_filter.py b/lessons/lesson-25/add_firewall_filter.py new file mode 100644 index 00000000..d9027911 --- /dev/null +++ b/lessons/lesson-25/add_firewall_filter.py @@ -0,0 +1,57 @@ +import grpc +import jnx_addr_pb2 as addr +import authentication_service_pb2 as auth +import authentication_service_pb2_grpc +import firewall_service_pb2 as fw +import firewall_service_pb2_grpc + +def add_firewall_filter(intf): + channel = grpc.insecure_channel('vqfx:32767') + auth_stub = authentication_service_pb2_grpc.LoginStub(channel) + response = auth_stub.LoginCheck( + auth.LoginRequest( + user_name='antidote', + password='antidotepassword', + client_id='jet', + ) + ) + + fw_stub = firewall_service_pb2_grpc.AclServiceStub(channel) + filter = fw.AccessList( + acl_name='filter-by-jet', + acl_type=fw.ACL_TYPE_CLASSIC, + acl_family=fw.ACL_FAMILY_INET, + acl_flag=fw.ACL_FLAGS_NONE, + ace_list=[ + fw.AclEntry(inet_entry=fw.AclInetEntry( + ace_name='t1', + ace_op=fw.ACL_ENTRY_OPERATION_ADD, + adjacency=fw.AclAdjacency(type=fw.ACL_ADJACENCY_AFTER), + matches=fw.AclEntryMatchInet(match_protocols=[fw.AclMatchProtocol(min=1, max=1, match_op=fw.ACL_MATCH_OP_EQUAL)]), + actions=fw.AclEntryInetAction( + action_t=fw.AclEntryInetTerminatingAction(action_accept=1), + actions_nt=fw.AclEntryInetNonTerminatingAction(action_log=1) + ) + )), + fw.AclEntry(inet_entry=fw.AclInetEntry( + ace_name='t2', + ace_op=fw.ACL_ENTRY_OPERATION_ADD, + adjacency=fw.AclAdjacency(type=fw.ACL_ADJACENCY_AFTER), + actions=fw.AclEntryInetAction( + action_t=fw.AclEntryInetTerminatingAction(action_discard=1), + actions_nt=fw.AclEntryInetNonTerminatingAction(action_log=1) + ), + )) + ] + ) + fw_stub.AccessListAdd(filter) + + fw_stub.AccessListBindAdd( + fw.AccessListObjBind( + acl=filter, + obj_type=fw.ACL_BIND_OBJ_TYPE_INTERFACE, + bind_object=fw.AccessListBindObjPoint(intf=intf), + bind_direction=fw.ACL_BIND_DIRECTION_INPUT, + bind_family=fw.ACL_FAMILY_INET + ) + ) diff --git a/lessons/lesson-25/stage1/configs/vqfx.txt b/lessons/lesson-25/stage1/configs/vqfx.txt index 47eedf1b..33a3b70f 100644 --- a/lessons/lesson-25/stage1/configs/vqfx.txt +++ b/lessons/lesson-25/stage1/configs/vqfx.txt @@ -18,7 +18,7 @@ system { } } services { - ssh { + ssh { root-login allow; } netconf { @@ -41,7 +41,7 @@ system { authorization info; } file interactive-commands { - interactive-commands any; + interactive-commands any; } } } diff --git a/lessons/lesson-25/stage1/guide.md b/lessons/lesson-25/stage1/guide.md index 34c91324..5711612f 100644 --- a/lessons/lesson-25/stage1/guide.md +++ b/lessons/lesson-25/stage1/guide.md @@ -16,20 +16,19 @@ JET supports the following: * An event notification method that enables the applications to respond to selected system events There are two types of services JET provides: -* Request-response - An application can issue a request and wait for the response from Junos OS. (RPC model, GRPC based) +* Request-response - An application can issue a request and wait for the response from Junos OS. (RPC model, gRPC based) * Notification - An application can receive asynchronous notification of events happening on Junos OS. (publish-subscribe model. MQTT based) -For more informations about the JET can be found [here](https://www.juniper.net/documentation/en_US/jet18.4/topics/concept/jet-architecture.html). +For more informations about the JET can be found here. -In this lab we are going to explore a off-box python JET applications. +In this lab we are going to explore a off-box python JET application. #### Config the Junos device for JET First of all, to run an off-box JET application, we need to enable the request-response configuration on the Junos OS device. -The gRPC can be run in clear-text mode _(insecure! that's why it is hidden and for lab test only -!)_ or SSL encrypted mode for enhanced security. For simpilicity we'll go with clear-text in the lab. More information about gRPC over SSL can be found [here](https://www.juniper.net/documentation/en_US/jet18.4/topics/topic-map/jet-off-box-apps.html). +The gRPC can be run in clear-text mode _(insecure! that's why it is hidden and for lab test only!)_ or SSL encrypted mode for enhanced security. For simpilicity we'll go with clear-text in the lab. More information about gRPC over SSL can be found here. -Apply below configuration to enable notification and GRPC request-response service on vQFX. +Apply below configuration to enable notification and gRPC request-response service on vQFX. ``` configure @@ -39,16 +38,11 @@ commit and-quit ``` -We can check the listening port to verify the rpc and notification service are enabled. +We can check the listening port to verify the notification and gRPC service are enabled. ``` show system connections | match LISTEN | match "\.1883|\.32767" ``` -Expected output -``` -> tcp4 0 0 *.1883 *.* LISTEN -> tcp46 0 0 *.32767 *.* LISTEN -``` -Now the Junos OS Device is ready for off-box JET applications and it's time to get some action! In the next chapter, we'll go through the notifiaction mechanism and collect some events from the MQTT event bus. +Now the Junos OS device is ready for off-box JET applications and it's time to get some action! In the next chapter, we'll go through the notifiaction mechanism and collect some events from the MQTT event bus. diff --git a/lessons/lesson-25/stage2/configs/vqfx.txt b/lessons/lesson-25/stage2/configs/vqfx.txt index 212af430..68bb87ca 100644 --- a/lessons/lesson-25/stage2/configs/vqfx.txt +++ b/lessons/lesson-25/stage2/configs/vqfx.txt @@ -18,7 +18,7 @@ system { } } services { - ssh { + ssh { root-login allow; } extension-service { @@ -56,7 +56,7 @@ system { authorization info; } file interactive-commands { - interactive-commands any; + interactive-commands any; } } } diff --git a/lessons/lesson-25/stage2/guide.md b/lessons/lesson-25/stage2/guide.md index db14b8a3..3ce7c4f4 100644 --- a/lessons/lesson-25/stage2/guide.md +++ b/lessons/lesson-25/stage2/guide.md @@ -14,12 +14,12 @@ Before we deep dive into notification service, let's get an introduction on mess A message broker system is hub and spoke based model where - Broker accepts messages from clients and delivers to other interested clients - Client either publish an message with a topic, or subscribe to a topic or both -- Topic is a namespace on the broker. Client subscribes or publish to a topics +- Topic is a namespace on the broker. Client subscribes or publish to a topics - Publish: a Client sending a message to a Broker with a topic - Subscribe: a Client requesting broker what topic it is interested.The broker will deliver messages to this client based on the topic it subscribed. #### JET Notification Service -Juniper JET notification service is a message broker system based on [MQTT](http://mqtt.org/) protocol to deliver system events. Junos system daemons such as RPD will generate messages and publish them to the JET message broker thru eventd with specific topics. For example, interface events (link up/down) will have topic ```/junos/events/kernel/interfaces``` while route table related event will have topic ```/junos/events/kernel/route-table```. The list of topic available can be found [here](https://www.juniper.net/documentation/en_US/jet17.4/topics/concept/jet-notification-api-overview.html). +Juniper JET notification service is a message broker system based on MQTT protocol to deliver system events. Junos system daemons such as RPD will generate messages and publish them to the JET message broker through eventd with specific topics. For example, interface events (link up/down) will have topic `/junos/events/kernel/interfaces` while route table related event will have topic `/junos/events/kernel/route-table`. The list of topic available can be found here. #### Creating a Python MQTT Client @@ -33,7 +33,7 @@ import paho.mqtt.client as mqtt ``` -After that, create a MQTT client object. Then define the `on_connect` callback function, which is triggered once the client connects to the JET MQTT broker, to subscribe the event. Here, we will subscribe to the topic `/junos/events/kernel/interfaces/ifa/add` which includes all new interfaces adress events. +After that, create a MQTT client object. Then define the `on_connect` callback function, which is triggered once the client connects to the JET MQTT broker, to subscribe the event. Here, we will subscribe to the topic `/junos/events/kernel/interfaces/ifa/add` which includes all new interfaces address events. At last we bind the on_connect function to the client object. ``` @@ -56,7 +56,7 @@ client.on_message = on_message ``` -Finally, instruct the client to connect to vQFX tcp 1883 we configured in previous stage, and start the main event loop function `loop_forever()` to wait for events. +Finally, instruct the client to connect to vQFX TCP port 1883 we configured in previous stage, and start the main event loop function `loop_forever()` to wait for events. ``` client.connect('vqfx', 1883, 60) @@ -78,33 +78,10 @@ commit and-quit ``` -Once the commit is completed, Eventd will receive the new IFA event and deliver it all clients who subscribed the IFA topic. +Once the commit is completed, Eventd will receive the new IFA event and deliver it to all clients who subscribed the IFA topic. Now change to `linux` terminal, you should be able to see the `KERNEL_EVENT_IFA_ADD` event. Press `Ctrl-C` to exit the loop. -Here's a sample output of the message: -``` - /junos/events/kernel/interfaces/ifa/add/xe-0/0/1.0/inet/192.168.20.1/32 { - "jet-event": { - "event-id": "KERNEL_EVENT_IFA_ADD", - "hostname": "vqfx", - "time": "2019-01-22-14:59:06", - "severity": "INFO", - "facility": "KERNEL", - "attributes": { - "name": "xe-0/0/0", - "subunit": 0, - "family": "inet", - "local-address": "192.168.10.1/32", - "destination-address": "192.168.10.0/24", - "broadcast-address": "192.168.10.255/32", - "generation-number": 144, - "flags": 192 - } - } - } -``` - In the next chapter we are going to explore JET request-response calls. diff --git a/lessons/lesson-25/stage3/configs/vqfx.txt b/lessons/lesson-25/stage3/configs/vqfx.txt index b382474a..e0d79efc 100644 --- a/lessons/lesson-25/stage3/configs/vqfx.txt +++ b/lessons/lesson-25/stage3/configs/vqfx.txt @@ -18,7 +18,7 @@ system { } } services { - ssh { + ssh { root-login allow; } extension-service { @@ -56,7 +56,7 @@ system { authorization info; } file interactive-commands { - interactive-commands any; + interactive-commands any; } } } diff --git a/lessons/lesson-25/stage3/guide.md b/lessons/lesson-25/stage3/guide.md index d6358783..e11b0143 100644 --- a/lessons/lesson-25/stage3/guide.md +++ b/lessons/lesson-25/stage3/guide.md @@ -8,21 +8,21 @@ #### Junos JET, gRPC and IDL -Juniper JET uses [gRPC](http://www.grpc.io/), a remote procedure call (RPC) framework, for cross-language services as a mechanism to enable request-response service. gRPC provides an interface definition language (IDL) that enables you to define APIs. These IDL files (with .proto as the file extension) are compiled using the protoc compiler to generate source code to be used for the server and client applications. The gRPC server is part of the JET service process (jsd), which runs on Junos OS. +Juniper JET uses gRPC, a remote procedure call (RPC) framework, for cross-language services as a mechanism to enable request-response service. gRPC provides an interface definition language (IDL) that enables you to define APIs. These IDL files (with .proto as the file extension) are compiled using the protoc compiler to generate source code to be used for the server and client applications. The gRPC server is part of the JET service process (jsd), which runs on Junos OS. ![JET System Archtecture](https://www.juniper.net/documentation/images/g043543.png) #### Compiling Junos IDL into python library -Junos IDL file can be downloaded from Juniper support webpage. For details on downloading and compiling the IDL please refer [here](https://www.juniper.net/documentation/en_US/jet1.0/topics/task/jet-complie-idl-using-thrift.html) +For details on downloading and compiling the IDL please refer here -For those who wants more details about gRPC in Python, there's a [quick start guide](https://grpc.io/docs/quickstart/python.html) grom grpc.io. +For those who wants more details about gRPC in Python, there's a quick start guide from grpc.io. -We will use the protoc compiler to compile the IDL file. You can download the IDL file from [Juniper support website](https://support.juniper.net/support/downloads/?p=jet). +We will use the protoc compiler to compile the IDL file. You can download the IDL file from Juniper support website. It's time to start the lab! -To save time, the IDL file is pre-downloaded already alrady. First we need to unarchive it: +To save time, the IDL file is pre-downloaded already. First we need to unarchive it: ``` cd /antidote/lessons/lesson-25 @@ -49,11 +49,7 @@ The python stub class is now ready, the next step we will use it to perform gRPC #### Python gRPC Client In this exercise we are going to create a simple python gRPC client to manipulate the Junos routing table. - -First, go to Python interactive prompt and import the required modules. - -`authentication_service_pb2` for the Junos Authentication -`rib_service_pb2` is the Junos JET RIB gRPC service. +First, go to Python interactive prompt and import the required modules - `authentication_service_pb2` for the Junos Authentication and `rib_service_pb2` is the Junos JET RIB gRPC service. ``` python @@ -67,7 +63,7 @@ import rib_service_pb2_grpc ``` -Now we create a gRPC tcp connection to the vQFX and perform the authentication. +Now we create a gRPC connection to the vQFX and perform the authentication. ``` channel = grpc.insecure_channel('vqfx:32767') @@ -90,7 +86,7 @@ print(response.result) ``` -Now, we can call the Route Add API to insert a static route dynamically. +Now, we can call the Route Add API to insert a static route 192.168.20.0/24 with next-hop 192.168.10.2 dynamically. ``` rib_stub = rib_service_pb2_grpc.RibStub(channel) @@ -126,4 +122,4 @@ show route table inet.0 ``` -Easy isn't it? With Juniper JET you can dynamically manage device configuration and runtime status such as route tables, ACL , interface status, etc without touching the CLI at all. This API interfaces open a whole new world for network applications. For more information about the API, please refer to the [JET Service APIs guide](https://www.juniper.net/documentation/en_US/jet18.2/topics/concept/jet-service-apis-overview.html) +Easy isn't it? With Juniper JET you can dynamically manage device configuration and runtime status such as route tables, ACL, interface status, etc without touching the CLI at all. This API interfaces open a whole new world for network applications. For more information about the API, please refer to the JET Service APIs guide diff --git a/lessons/lesson-25/stage4/configs/vqfx.txt b/lessons/lesson-25/stage4/configs/vqfx.txt index a867ac9b..27ff84ae 100644 --- a/lessons/lesson-25/stage4/configs/vqfx.txt +++ b/lessons/lesson-25/stage4/configs/vqfx.txt @@ -18,7 +18,7 @@ system { } } services { - ssh { + ssh { root-login allow; } extension-service { @@ -56,7 +56,7 @@ system { authorization info; } file interactive-commands { - interactive-commands any; + interactive-commands any; } } } diff --git a/lessons/lesson-25/stage4/guide.md b/lessons/lesson-25/stage4/guide.md index bb1a6f60..46121d05 100644 --- a/lessons/lesson-25/stage4/guide.md +++ b/lessons/lesson-25/stage4/guide.md @@ -37,7 +37,7 @@ response = auth_stub.LoginCheck( ``` -We simulate a neighboring device by creating a new routing instance in the vQFX. Interface xe-0/0/1 and xe-0/0/2 is connected together, so on vQFX we can ping from the master routing instance to 192.168.10.2, which is the IP address in the routing instance "VR". Let's try to verify it: +We simulate a neighboring device by creating a new routing instance in the vQFX. Interface xe-0/0/0 and xe-0/0/1 is connected together, so on vQFX we can ping from the master routing instance to 192.168.10.2, which is the IP address in the routing instance "VR". Let's try to verify it: ``` ping 192.168.10.2 count 3 @@ -45,7 +45,7 @@ ping 192.168.10.2 count 3 #### Create ACL via JET -Now, we use the JET firewall API to create a new firewall filter call "filter-by-jet". This filter contains two terms, the first one is to log and permit ICMP traffic, and the last one is to log and discard all traffic. +Now, we use the JET firewall API to create a new firewall filter call `filter-by-jet`. This filter contains two terms, the first one is to log and permit ICMP traffic, and the last one is to log and discard all traffic. ``` fw_stub = firewall_service_pb2_grpc.AclServiceStub(channel) @@ -129,4 +129,4 @@ show firewall log ``` -This concludes ourJunos JET gRPC demostration. In the next lesson are we going explore closed loop automation by employing both JET Notification Service and JET RPC. +This concludes our Junos JET gRPC demostration. In the next lesson are we going explore closed loop automation by employing both JET Notification Service and JET RPC. diff --git a/lessons/lesson-25/stage5/configs/vqfx.txt b/lessons/lesson-25/stage5/configs/vqfx.txt new file mode 100644 index 00000000..66361b9e --- /dev/null +++ b/lessons/lesson-25/stage5/configs/vqfx.txt @@ -0,0 +1,109 @@ +version 17.4R1.16; +system { + host-name vqfx; + root-authentication { + encrypted-password "$6$yGARIqqs$.pxKXsYWySQy8BzpcuynxpBzXd7Z2tjqHlCR9wIS/MkEl1NrAsV/ZwNrJCtCrWQOovlmEgPvWXRzcPAykvCY8/"; ## SECRET-DATA + } + login { + user antidote { + uid 2000; + class super-user; + authentication { + encrypted-password "$6$L1CNVBaS$lxgFvGaqn5ixBoCNfNod44gQ26.3IYI.hurq/pL0a9Gqq8837HfP0iMZ7SKpzTF34wNmbK4axz4EIijAtLHRl0"; ## SECRET-DATA + } + } + password { + change-type set-transitions; + minimum-changes 0; + } + } + services { + ssh { + root-login allow; + } + extension-service { + request-response { + grpc { + clear-text { + port 32767; + } + } + } + notification { + port 1883; + allow-clients { + address 0.0.0.0/0; + } + } + } + netconf { + ssh; + rfc-compliant; + } + rest { + http { + port 8080; + } + enable-explorer; + } + } + syslog { + user * { + any emergency; + } + file messages { + any notice; + authorization info; + } + file interactive-commands { + interactive-commands any; + } + } +} +interfaces { + xe-0/0/0 { + unit 0 { + family inet { + address 192.168.10.1/24; + } + } + } + xe-0/0/1 { + unit 0 { + family inet { + address 192.168.10.2/24; + } + } + } + xe-0/0/3 { + unit 0 { + family inet { + address 20.1.1.2/24; + } + } + } + em0 { + unit 0 { + family inet { + address 10.0.0.15/24; + } + } + } + em1 { + unit 0 { + family inet { + address 169.254.0.2/24; + } + } + } +} +routing-instances { + VR { + instance-type virtual-router; + interface xe-0/0/1.0; + } + VR2 { + instance-type virtual-router; + interface xe-0/0/3.0; + } +} diff --git a/lessons/lesson-25/stage5/guide.md b/lessons/lesson-25/stage5/guide.md index d8d89605..25aa9087 100644 --- a/lessons/lesson-25/stage5/guide.md +++ b/lessons/lesson-25/stage5/guide.md @@ -5,3 +5,86 @@ --- ### Chapter 5 - Closed loop automation with JET + +In this stage, we try to combine what we have learnt previously to demonstrate a closed-loop automation script. + +We use JET notification services to listen for new IFA event and if the configured IP address is a public IP address, we use gRPC service to provision a firewall filter to protect the vQFX device. + +Here, we put the JET firewall API code in stage 4 in a separate file `add_firewall_filter.py`. Try to view the code now. + +``` +cd /antidote/lessons/lesson-25 +cat add_firewall_filter.py +``` + + +Again, we repeat what we have done in previous stage - compile the IDL package, go to Python interactive prompt, import the required module. + +``` +tar -xzf jet-idl-17.4R1.16.tar.gz +python -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. ./proto/*.proto + +python +import json +import ipaddress +import paho.mqtt.client as mqtt +from add_firewall_filter import add_firewall_filter + +``` + + +Same as stage 2, we create the MQTT client and define `on_connect` callback function to subscribe the IFA add event once clients connect to the JET MQTT broker. + +``` +client = mqtt.Client() + +def on_connect(client, userdata, flags, rc): + client.subscribe("/junos/events/kernel/interfaces/ifa/add/#") + +client.on_connect = on_connect +``` + + +Next, define `on_message` callback function, to call the `add_firewall_filter` function if the new IP address is in the public range. After that, connect to vQFX and start to wait for event. + +``` +def on_message(client, userdata, msg): + payload = json.loads(msg.payload) + intf = '%s.%s' % (payload['jet-event']['attributes']['name'], + payload['jet-event']['attributes']['subunit']) + ipaddr = payload['jet-event']['attributes']['local-address'] + if not ipaddress.IPv4Network(ipaddr).is_private: + print('Apply firewall filter on interface %s' % intf) + add_firewall_filter(intf) + else: + print('No action for private IP %s' % ipaddr.split('/')[0]) + +client.on_message = on_message + +client.connect('vqfx', 1883, 60) +client.loop_forever() +``` + + +Here, We simulate another neighboring device by creating one more routing instance in the vQFX. Interface xe-0/0/2 and xe-0/0/3 is connected together, and a public IP address `20.1.1.2/24` is pre-configured on xe-0/0/3 which is in routing instance `VR2` + +Now, let's create IP address on xe-0/0/2 interface. + +``` +configure +set interfaces xe-0/0/2 unit 0 family inet address 20.1.1.1/24 +commit and-quit +``` + + +Verify the message `Apply firewall filter ...` is shown in `linux` terminal. + +To verify the firewall ACL is being applied, we ping 20.1.1.2 again and then check the firewall log. + +``` +ping 20.1.1.2 count 3 +show firewall log +``` + + +This concludes our closed-loop automation using JET services. From 31c77e291b065c11bfc35bb47d334e86284c30ef Mon Sep 17 00:00:00 2001 From: Tony Chan Date: Tue, 5 Feb 2019 11:07:23 +1100 Subject: [PATCH 4/5] Updated stage 5 --- lessons/lesson-25/stage5/guide.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lessons/lesson-25/stage5/guide.md b/lessons/lesson-25/stage5/guide.md index 25aa9087..145904db 100644 --- a/lessons/lesson-25/stage5/guide.md +++ b/lessons/lesson-25/stage5/guide.md @@ -6,11 +6,12 @@ ### Chapter 5 - Closed loop automation with JET -In this stage, we try to combine what we have learnt previously to demonstrate a closed-loop automation script. +In this lesson, we are going to combine what we have learnt previously to create a closed-loop automation script. -We use JET notification services to listen for new IFA event and if the configured IP address is a public IP address, we use gRPC service to provision a firewall filter to protect the vQFX device. +The objective of this lesson is to use JET notification services to listen for any new interface addresses configured(IFA events). If the configured IP address is a public one, we use gRPC service to provision a firewall filter on the corresponding interface to protect the vQFX device. -Here, we put the JET firewall API code in stage 4 in a separate file `add_firewall_filter.py`. Try to view the code now. +#### Create the close loop automation script +To save time, we have already put the JET firewall API code we created in stage 4 in a file `add_firewall_filter.py`. Let's take a look on the code: ``` cd /antidote/lessons/lesson-25 @@ -33,7 +34,7 @@ from add_firewall_filter import add_firewall_filter ``` -Same as stage 2, we create the MQTT client and define `on_connect` callback function to subscribe the IFA add event once clients connect to the JET MQTT broker. +Then same as stage 2, we create a MQTT client and define `on_connect` callback function to subscribe to the topic "IFA add events" once the client connect to the JET MQTT broker. ``` client = mqtt.Client() @@ -66,9 +67,10 @@ client.loop_forever() ``` -Here, We simulate another neighboring device by creating one more routing instance in the vQFX. Interface xe-0/0/2 and xe-0/0/3 is connected together, and a public IP address `20.1.1.2/24` is pre-configured on xe-0/0/3 which is in routing instance `VR2` +#### Testing the close loop automation +To validate our automation, We simulate another neighboring device by creating one more routing instance in the vQFX. Interface xe-0/0/2 and xe-0/0/3 is connected together, and a public IP address `20.1.1.2/24` is pre-configured on xe-0/0/3 which is in routing instance `VR2` -Now, let's create IP address on xe-0/0/2 interface. +Now, let's create an IP address on xe-0/0/2 interface. ``` configure @@ -87,4 +89,8 @@ show firewall log ``` -This concludes our closed-loop automation using JET services. +The firewall log should capture the ping traffic. This verifies a firewall filter can be automatically provisioned to the interface dynamically without modifying the Junos configuration. + +This JET API automation capability opens up a whole new world to design and develop business logics within the network, such as customized traffic engineering, dynamic network protection, and so on. + +This concludes our introduction to closed-loop automation using JET services and we hope you enjoy this course! From 73e5c97c25afcd8fc5e5dd2c9155bc059a846e5d Mon Sep 17 00:00:00 2001 From: Raymond Lam Date: Tue, 19 Mar 2019 13:11:15 +0800 Subject: [PATCH 5/5] Update according to Matt's comments --- lessons/lesson-25/stage1/guide.md | 4 ++-- lessons/lesson-25/stage2/guide.md | 2 +- lessons/lesson-25/stage3/guide.md | 2 +- lessons/lesson-25/stage4/guide.md | 2 +- lessons/lesson-25/stage5/guide.md | 2 +- lessons/lesson-25/syringe.yaml | 3 +-- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lessons/lesson-25/stage1/guide.md b/lessons/lesson-25/stage1/guide.md index 5711612f..5369c477 100644 --- a/lessons/lesson-25/stage1/guide.md +++ b/lessons/lesson-25/stage1/guide.md @@ -1,4 +1,4 @@ -## Automating with JET +## Juniper Extension Toolkit (JET) **Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** @@ -26,7 +26,7 @@ In this lab we are going to explore a off-box python JET application. #### Config the Junos device for JET First of all, to run an off-box JET application, we need to enable the request-response configuration on the Junos OS device. -The gRPC can be run in clear-text mode _(insecure! that's why it is hidden and for lab test only!)_ or SSL encrypted mode for enhanced security. For simpilicity we'll go with clear-text in the lab. More information about gRPC over SSL can be found here. +The gRPC can be run in clear-text mode _(insecure! that's why it is hidden and for lab test only!)_ or SSL encrypted mode for enhanced security. For simplicity we'll go with clear-text in the lab. More information about gRPC over SSL can be found here. Apply below configuration to enable notification and gRPC request-response service on vQFX. diff --git a/lessons/lesson-25/stage2/guide.md b/lessons/lesson-25/stage2/guide.md index 3ce7c4f4..c1db4007 100644 --- a/lessons/lesson-25/stage2/guide.md +++ b/lessons/lesson-25/stage2/guide.md @@ -1,4 +1,4 @@ -## Automating with JET +## Juniper Extension Toolkit (JET) **Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** diff --git a/lessons/lesson-25/stage3/guide.md b/lessons/lesson-25/stage3/guide.md index e11b0143..0ed2ebee 100644 --- a/lessons/lesson-25/stage3/guide.md +++ b/lessons/lesson-25/stage3/guide.md @@ -1,4 +1,4 @@ -## Automating with JET +## Juniper Extension Toolkit (JET) **Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** diff --git a/lessons/lesson-25/stage4/guide.md b/lessons/lesson-25/stage4/guide.md index 46121d05..de72f299 100644 --- a/lessons/lesson-25/stage4/guide.md +++ b/lessons/lesson-25/stage4/guide.md @@ -1,4 +1,4 @@ -## Automating with JET +## Juniper Extension Toolkit (JET) **Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** diff --git a/lessons/lesson-25/stage5/guide.md b/lessons/lesson-25/stage5/guide.md index 145904db..e025c40c 100644 --- a/lessons/lesson-25/stage5/guide.md +++ b/lessons/lesson-25/stage5/guide.md @@ -1,4 +1,4 @@ -## Automating with JET +## Juniper Extension Toolkit (JET) **Contributed by: [@valjeanchan](https://github.com/valjeanchan) and [@jnpr-raylam](https://github.com/jnpr-raylam)** diff --git a/lessons/lesson-25/syringe.yaml b/lessons/lesson-25/syringe.yaml index 794238a4..52c15bd1 100644 --- a/lessons/lesson-25/syringe.yaml +++ b/lessons/lesson-25/syringe.yaml @@ -1,9 +1,8 @@ --- -lessonName: Automating with Junos Extension Toolkit (JET) +lessonName: Juniper Extension Toolkit (JET) lessonId: 25 category: configuration tier: local -juniperSpecific: true utilities: - name: linux