{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "This inbound_network sample application walk you through how to enable inbound networking port on Panorama devices, and how to run a simple HTTP server within a Panorama application. In order to focus on the inbound networking topic, this sample application doesn't use any cameras and any models.\n", "\n", "#### How this application works\n", "\n", "1. Configure inbound networking port information and port mapping between host and container in JSON files, before deployment.\n", "1. In the application, start a thread to serve HTTP requests, and react to HTTP GET requests to some predefined paths. ( \"/py_object_stat\", \"/py_threads\" ).\n", "1. Confirm you can see the expected contents by opening the URLs with your PC browser.\n", "\n", "## Prerequisites\n", "\n", "Before you start processing this notebook, some prerequisites need to be completed.\n", "\n", "* Set up your AWS Panorama Appliance - [middle click to open document](https://docs.aws.amazon.com/panorama/latest/dev/gettingstarted-setup.html)\n", "* Install \"panorama-cli\" tool [middle click to open document](https://docs.aws.amazon.com/panorama/latest/dev/gettingstarted-deploy.html#gettingstarted-deploy-prerequisites)\n", "\n", "
Security Warning: Turning on a device port may create a security vulnerability. This sample opens a port to run simple HTTP server just to explain the inbound networking feature. When you open a port in your own application, please be careful not to cause leaking sensitive information, or breaking your application's behavior.
\n" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Import libraries and define configurations\n", "\n", "First step is to import all libraries needed." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys\n", "import os\n", "import time\n", "import json\n", "import uuid\n", "\n", "import boto3\n", "\n", "sys.path.insert( 0, os.path.abspath( \"../common/test_utility\" ) )\n", "import panorama_test_utility" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You need to specify some information specific to your environment." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n", "region_name = boto3.session.Session().region_name\n", "\n", "print( \"account_id :\", account_id )\n", "print( \"region_name :\", region_name )\n", "\n", "# Following configurations are required when you use real hardware, \n", "# thus can be any dummy strings when you use only Test Utility.\n", "device_id = input(\"Device Id (format : device-*)\").strip()" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Import application\n", "\n", "With \"panorama-cli import-application\" command, replacing placeholder information in application files. This step essentially replace placeholder (\"123456789012\") with your aws account id." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!cd ./inbound_network_app/ && panorama-cli import-application" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prepare business logic\n", "\n", "#### Preview python source code\n", "Next step is to build a business logic container. This application's business logic consists of single python source code. Let's preview it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, "outputs": [], "source": [ "panorama_test_utility.preview_text_file( f\"./inbound_network_app/packages/{account_id}-inbound_network_code-1.0/src/app.py\" )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `Application` class just creates and start a `IntrospectionHttpServerThread` instance, and run main loop without using cameras, models, and HDMI output. This application focuses on the explanation of inbound networking feature.\n", "\n", "---------\n", "``` python\n", "# Start a http server thread\n", "self.http_server_thread = IntrospectionHttpServerThread()\n", "self.http_server_thread.start()\n", "```\n", "---------\n", "\n", "\n", "In the `IntrospectionHttpServerThread.run()`, it creates a `TCPServer` with `IntrospectionHttpRequestHandler` with port number `8080`.\n", "\n", "---------\n", "``` python\n", "PORT = 8080\n", "with socketserver.TCPServer((\"\", PORT), IntrospectionHttpRequestHandler) as httpd:\n", " :\n", "```\n", "---------\n", "\n", "\n", "`IntrospectionHttpRequestHandler.do_GET()` handles HTTP GET requests to \"/py_object_stat\" and \"/py_threads\". For other paths it returns 404 error.\n", "\n", "---------\n", "``` python\n", "if self.path==\"/py_object_stat\":\n", "\n", " self.send_response(200)\n", " self.send_header('Content-Type', 'text/plain')\n", " self.end_headers()\n", "\n", " message = get_py_object_stat()\n", " message = message.encode(\"utf-8\")\n", "\n", " self.wfile.write(message)\n", " :\n", "```\n", "---------\n", "\n", "`get_py_object_stat()` is a helper functions to return numbers of python objects for each type in a string.\n", "`get_py_threads()` is a helper functions to return call-stacks of all python threads in a string.\n", "These functions are used to generate response body of HTTP requests.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Test run the business logic with test-utility\n", "\n", "Let's run the application with Test Utility **Run** command. In this sample, we don't use any model, so model compilation step is not needed.\n", "\n", "Now the application is running HTTP server at port 8080. Please open a new tab on your browser, and open URLs. `http://{ip address}:8080/py_object_stat` or `http://{ip address}:8080/py_threads`, and make sure you can see numbers of python objects and call-stacks.\n", "\n", "**Note:** depending on your environment such as firewall setting, port 8080 may not be accessible from your browser. In that case please move on to next steps without confirming the result here.\n", "\n", "
How to stop: This application runs infinitely, and doesn't end automatically. From the menu bar, please select \"Kernel\" > \"Interrupt Kernel\" to abort the application.
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, "outputs": [], "source": [ "# Run the application with test-utility.\n", "\n", "%run ../common/test_utility/panorama_test_utility_run.py \\\n", "--app-name inbound_network_app \\\n", "--code-package-name inbound_network_code \\\n", "--py-file ./inbound_network_app/packages/{account_id}-inbound_network_code-1.0/src/app.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Build application logic container\n", "\n", "With \"panorama-cli build-container\" command, building a container image, and add it into the \"inbound_network_code\" package.\n", "\n", "This step takes long time (5~10 mins), and because it is using %%capture magic command, you don't see progress during the process. Please wait." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, "outputs": [], "source": [ "%%capture captured_output\n", "# FIXME : without %%capture, browser tab crashes because of too much output from the command.\n", "\n", "!cd ./inbound_network_app && panorama-cli build-container \\\n", " --container-asset-name code \\\n", " --package-path packages/{account_id}-inbound_network_code-1.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stdout_lines = captured_output.stdout.splitlines()\n", "stderr_lines = captured_output.stderr.splitlines()\n", "print(\" :\")\n", "print(\" :\")\n", "for line in stdout_lines[-30:] + stderr_lines[-30:]:\n", " print(line)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Define inbound networking port in the code package JSON file\n", "\n", "In order to enable inbound networking port, you need to include necessary information in 2 places.\n", "1. package.json of the code package.\n", "2. override manifest file.\n", "\n", "Let's make sure `package.json` of the code package contains the port number we want to use. In this sample, the package.json file already contains \"nodePackage\" > \"interfaces\" > \"network\" > \"inboundPorts\" element.\n", "\n", "``` json\n", "\"network\": {\n", " \"inboundPorts\": [\n", " {\n", " \"port\": 8080,\n", " \"description\": \"http\"\n", " }\n", " ]\n", "}\n", "```\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "panorama_test_utility.preview_text_file( f\"./inbound_network_app/packages/{account_id}-inbound_network_code-1.0/package.json\" )" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Package application (upload locally prepared packages onto Cloud)\n", "\n", "Now you have prepared code package locally, and confirmed that package.json contains \"inboundPorts\" element. Let's upload the package to the cloud with \"panorama-cli package-application\" command." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!cd ./inbound_network_app && panorama-cli package-application" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Deploy application to the device programatically\n", "\n", "Once you uploaded the package to the cloud, you can create an application instance on your device. You need to specify a manifest file and an override manifest file.\n", "\n", "As explained above, you need to make sure that override manifest file contains necessary information. In this sample, the override.json file already contains \"nodeGraphOverrides\" > \"networkRoutingRules\" element, and it contains a mapping between the container port (8080) and host port (8081). This means application's business logic container uses port number 8080, and it is mapped to port number 8081 of the device.\n", "\n", "\n", "**Note:** \"networkRoutingRules\" has to be defined in the **override** manifest file, not in the main manifest file. \"networkRoutingRules\" in the main manifest file is used only to provide default values when you deploy your application via the Management Console UI.\n", "\n", "``` json\n", "\"networkRoutingRules\":[\n", " {\n", " \"node\": \"code_node\",\n", " \"containerPort\": 8080,\n", " \"hostPort\": 8081\n", " }\n", "]\n", "```\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "panorama_test_utility.preview_text_file( \"./inbound_network_app/graphs/inbound_network_app/graph.json\" )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "panorama_test_utility.preview_text_file( \"./inbound_network_app/graphs/inbound_network_app/override.json\" )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Deploy the app using the manifest file and override manifest file\n", "\n", "In order to create an application instance, this notebook uses boto3's \"panorama\" client and its create_application_instance() API. (It is also possible to use \"aws panorama create-application-instance\" command instead.)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# create a boto3 client to access Panorama service\n", "# FIXME : not specifying region name here, because panorama-cli uses only default region currently.\n", "panorama_client = boto3.client(\"panorama\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def deploy_application( application_name, manifest_filename, override_filename ):\n", "\n", " def get_payload_from_json( filename ):\n", " with open( filename ) as fd:\n", " \n", " s = fd.read()\n", " \n", " # validating JSON format and making it compact, by loading and dumping, \n", " payload = json.dumps(json.loads(s))\n", " \n", " return payload\n", "\n", " manifest_payload = get_payload_from_json( manifest_filename )\n", " \n", " params = {\n", " \"Name\" : application_name,\n", " \"DefaultRuntimeContextDevice\" : device_id,\n", " \"ManifestPayload\" : {\"PayloadData\":manifest_payload},\n", " }\n", " \n", " if override_filename:\n", " override_payload = get_payload_from_json( override_filename )\n", " params[\"ManifestOverridesPayload\"] = {\"PayloadData\":override_payload}\n", " \n", " response = panorama_client.create_application_instance( ** params )\n", " \n", " return response" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "application_name = \"inbound_network_notebook_\" + str(uuid.uuid4())[:8]\n", "\n", "response = deploy_application(\n", " application_name = application_name,\n", " manifest_filename = \"./inbound_network_app/graphs/inbound_network_app/graph.json\",\n", " override_filename = \"./inbound_network_app/graphs/inbound_network_app/override.json\",\n", ")\n", "\n", "application_instance_id = response[\"ApplicationInstanceId\"]\n", "\n", "response" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "application_name" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "#### Wait for deployment completion\n", "\n", "Application instance creation has been triggered. This notebook checks the progress by calling describe_application_instance() API periodically. Please confirm that you see \"DEPLOYMENT_SUCCEEDED\" status at the end." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def wait_deployment( application_instance_id ):\n", " \n", " progress_dots = panorama_test_utility.ProgressDots() \n", " while True:\n", " app = panorama_client.describe_application_instance( ApplicationInstanceId = application_instance_id )\n", " progress_dots.update_status( \"%s (%s)\" % (app[\"Status\"], app[\"StatusDescription\"]) )\n", " if app[\"Status\"] not in ( \"DEPLOYMENT_PENDING\", \"DEPLOYMENT_REQUESTED\", \"DEPLOYMENT_IN_PROGRESS\" ):\n", " break\n", " time.sleep(60)\n", "\n", "wait_deployment( application_instance_id )" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "#### Visit CloudWatch Logs to check logs from the application instance\n", "\n", "If you saw \"DEPLOYMENT_SUCCEEDED\" status, the application started to run on your device. Application logs are uploaded to CloudWatch Logs. Let's get the URL of CloudWatch Logs management console. \"console_output\" is the log stream your Python code's stdout/stderr are redirected to." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "logs_url = panorama_test_utility.get_logs_url( region_name, device_id, application_instance_id )\n", "print( \"CloudWatch Logs URL :\" )\n", "print( logs_url )" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "#### Open HTTP URLs on the Panorama device\n", "\n", "Let's open a new tab on your browser, and open following HTTP URLs.\n", "\n", "* `http://{ip-address-of-the-device}:8081/py_object_stat`\n", "* `http://{ip-address-of-the-device}:8081/py_threads`\n", "\n", "You can check the ip address of your device either on the Management Console Device Setting screen, or by executing following cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = panorama_client.describe_device( DeviceId = device_id )\n", "response[\"CurrentNetworkingStatus\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\"screenshot1\"\n", "\"screenshot2\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Remove the application instance from the device\n", "\n", "Once you confirm that this application is running as expected, we can remove it from the device. Please enter Y or N in the text box." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "answer = input(\"Remove the application? [yN]\")\n", "if answer.lower()==\"y\":\n", " panorama_test_utility.remove_application( device_id, application_instance_id )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "interpreter": { "hash": "0bf254f4bade2f1c26977f6424deaa54afd031ced29e33743fcf1b047c1a16ff" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.5" } }, "nbformat": 4, "nbformat_minor": 4 }