diff --git a/.gitignore b/.gitignore index 2129b63..dd7149c 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ cython_debug/ #.idea/ .venv/ -.DS_Store \ No newline at end of file +.DS_Store +.vscode \ No newline at end of file diff --git a/cloudapp/app/app.py b/cloudapp/app/app.py index 7fc40d3..394b775 100644 --- a/cloudapp/app/app.py +++ b/cloudapp/app/app.py @@ -39,9 +39,7 @@ def echo(): 'env': app.config['site'], 'info': { 'method': request.method, - 'url': request.url, - 'path': request.path, - 'full_path': request.full_path + 'path': request.path } } if data: @@ -78,6 +76,10 @@ def echo_html(): } return render_template('pretty_echo.html', request_env=app.config['site'], info=info, request_headers=headers, request_data=data) + @app.route('/foo/', methods=['GET']) + def ex_test(): + return jsonify({'info': 'bar'}) + return app app = create_app() diff --git a/labapp/app/app.py b/labapp/app/app.py index 8510986..d6d720a 100644 --- a/labapp/app/app.py +++ b/labapp/app/app.py @@ -43,18 +43,20 @@ def eph_ns() -> str: this_eph_ns = request.cookies.get('eph_ns', None) return this_eph_ns -def cloudapp_fetch(url, timeout, prop, value, headers = {}): +def cloudapp_fetch(session, url, timeout, prop, value, headers = {}): """ Fetch data from URL Validate prop and value in the JSON response """ - response = requests.get(url, timeout=timeout, headers=headers) + response = session.get(url, timeout=timeout) response.raise_for_status() data = response.json() if data.get(prop) != value: raise ValueError(f"Invalid {prop}: expected {value}, got {data.get(prop)}") - clean_headers = headers_cleaner(data['request_headers']) - data['request_headers'] = clean_headers + if data.get("request_headers"): + clean_headers = headers_cleaner(data['request_headers']) + data['request_headers'] = clean_headers + return data return data def headers_cleaner(headers): @@ -81,16 +83,25 @@ def return_err(err): @app.route('/') def index(): """index page""" - html = render_md("markdown/overview.md") + html = render_md("markdown/welcome.md") return render_template('standard.html', title="MCN Practical: Overview", - content=html, - udf=app.config['UDF'] + content=html + ) + +@app.route('/overview') +def arch(): + """arch page""" + html = render_md("markdown/overview.md") + return render_template('standard.html', + title="MCN Practical: Architecture", + content=html ) @app.route('/setup', methods=['GET', 'POST']) def setup(): """setup page""" + ns = eph_ns() if request.method == 'POST': action = request.form['action'] if action == 'save': @@ -100,7 +111,7 @@ def setup(): return redirect(url_for('setup')) response = make_response(redirect('/setup')) response.set_cookie('eph_ns', this_eph_ns, max_age=60*60*24) - flash("Ephemeral NS successfully set.", "success") + flash('Ephemeral NS successfully set.', "success") return response if action == 'clear': response = make_response(redirect('/setup')) @@ -111,17 +122,7 @@ def setup(): return render_template('setup.html', title="MCN Practical: Setup", content=html, - udf=app.config['UDF'] - ) - -@app.route('/arch') -def arch(): - """arch page""" - html = render_md("markdown/arch.md") - return render_template('standard.html', - title="MCN Practical: Architecture", - content=html, - udf=app.config['UDF'] + ns=ns ) @app.route('/_ce_status') @@ -139,8 +140,7 @@ def lb(): return render_template('exercise_standard.html', title="MCN Practical: LB", content=html, - ns=ns, - udf=app.config['UDF'] + ns=ns ) @app.route('/route') @@ -152,30 +152,108 @@ def path(): title="MCN Practical: HTTP Routing", content=html, ns=ns, - udf=app.config['UDF'] + ) -@app.route('/header') +@app.route('/manipulation') def header(): - """header page""" + """manipulation page""" ns = eph_ns() - html = render_md("markdown/header.md") + html = render_md("markdown/manipulation.md") return render_template('exercise_standard.html', - title="MCN Practical: Headers", + title="MCN Practical: Manipulation", content=html, - ns=ns, - udf=app.config['UDF'] + ns=ns + ) + +@app.route('/portability') +def port(): + """portability page""" + ns = eph_ns() + html = render_md("markdown/portability.md") + return render_template('exercise_standard.html', + title="MCN Practical: Portability", + content=html, + ns=ns + ) + +@app.route('/vnet') +def vnet(): + """reference page""" + ns = eph_ns() + html = render_md("markdown/reference.md") + return render_template('coming-soon.html', + title="MCN Practical: Reference", + content=html, + ns=ns + ) + +@app.route('/netpolicy') +def netp(): + """reference page""" + ns = eph_ns() + html = render_md("markdown/reference.md") + return render_template('coming-soon.html', + title="MCN Practical: Reference", + content=html, + ns=ns + ) + +@app.route('/ref') +def ref(): + """reference page""" + ns = eph_ns() + html = render_md("markdown/reference.md") + return render_template('coming-soon.html', + title="MCN Practical: Reference", + content=html, + ns=ns ) +@app.route('/score') +def score(): + """scoreboard page""" + ns = eph_ns() + html = render_md("markdown/score.md") + return render_template('coming-soon.html', + title="MCN Practical: Scoreboard", + content=html, + ns=ns + ) + +@app.route('/_test1') +def ex_test(): + """Example test""" + try: + s = requests.Session() + url = f"https://foo.{app.config['base_url']}/" + data = cloudapp_fetch(s, url, 5, 'info', 'bar') + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: + return jsonify(status='fail', error=str(e)) + +@app.route('/_test2') +def ex_test2(): + """Example test""" + try: + s = requests.Session() + url = f"https://bar.{app.config['base_url']}/" + data = cloudapp_fetch(s, url, 5, 'info', 'foo') + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: + return jsonify(status='fail', error=str(e)) + + @app.route('/_lb1') def lb_aws(): """Azure LB test""" try: + s = requests.Session() ns = eph_ns() if not ns: raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}" - data = cloudapp_fetch(url, 5, 'env', 'AWS') + data = cloudapp_fetch(s, url, 5, 'env', 'AWS') return jsonify(status='success', data=data) except (LabException, requests.RequestException, ValueError) as e: return jsonify(status='fail', error=str(e)) @@ -184,11 +262,12 @@ def lb_aws(): def lb_azure(): """Azure LB test""" try: + s = requests.Session() ns = eph_ns() if not ns: raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}" - data = cloudapp_fetch(url, 5, 'env', 'Azure') + data = cloudapp_fetch(s, url, 5, 'env', 'Azure') return jsonify(status='success', data=data) except (LabException, requests.RequestException, ValueError) as e: return jsonify(status='fail', error=str(e)) @@ -197,14 +276,15 @@ def lb_azure(): def route1(): """First Route Test""" try: + s = requests.Session() ns = eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] aws_url = f"https://{ns}.{base_url}/aws/raw" azure_url = f"https://{ns}.{base_url}/azure/raw" - aws_data = cloudapp_fetch(aws_url, 5, 'env', 'AWS') - azure_data = cloudapp_fetch(azure_url, 5, 'env', 'Azure') + aws_data = cloudapp_fetch(s, aws_url, 5, 'env', 'AWS') + azure_data = cloudapp_fetch(s, azure_url, 5, 'env', 'Azure') data = { "aws": aws_data, "azure": azure_data @@ -217,14 +297,17 @@ def route1(): def route2(): """First Route Test""" try: + s = requests.Session() ns = eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] aws_url = f"https://{ns}.{base_url}/" azure_url = f"https://{ns}.{base_url}/" - aws_data = cloudapp_fetch(aws_url, 5, 'env', 'AWS', headers={"X-MCN-lab": "aws"}) - azure_data = cloudapp_fetch(azure_url, 5, 'env', 'Azure', headers={"X-MCN-lab": "azure"}) + s.headers["X-MCN-lab"] = "aws" + aws_data = cloudapp_fetch(s, aws_url, 5, 'env', 'AWS') + s.headers["X-MCN-lab"] = "azure" + azure_data = cloudapp_fetch(s, azure_url, 5, 'env', 'Azure', headers={"X-MCN-lab": "azure"}) data = { "aws": aws_data, "azure": azure_data @@ -232,6 +315,21 @@ def route2(): return jsonify(status='success', data=data) except (LabException, requests.RequestException, ValueError) as e: return jsonify(status='fail', error=str(e)) + +@app.route('/_manip1') +def manip1(): + """First Manip Test""" + try: + s = requests.Session() + ns = eph_ns() + if not ns: + raise LabException("Ephemeral NS not set") + base_url = app.config['base_url'] + url = f"https://{ns}.{base_url}/aws/raw" + r_data = cloudapp_fetch(s, url, 5, 'info', '{"path": "/"}') + return jsonify(status='success', data=r_data) + except (LabException, requests.RequestException, ValueError) as e: + return jsonify(status='fail', error=str(e)) if __name__ == '__main__': diff --git a/labapp/app/markdown/arch.md b/labapp/app/markdown/arch.md deleted file mode 100644 index 52f46a9..0000000 --- a/labapp/app/markdown/arch.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# **Architecture** - - - - -
- - Descriptive Text - -
diff --git a/labapp/app/markdown/header.md b/labapp/app/markdown/header.md deleted file mode 100644 index 01be8b0..0000000 --- a/labapp/app/markdown/header.md +++ /dev/null @@ -1,44 +0,0 @@ - - -# **Header Manipulation** - - - -
- -### **Exercise 1: Add/Remove** - -HERE - -
- -
-
- - -
- -Nice 🚀! If you've completed all the exercises so far, you have a good foundation for how App Connect addresses common L7 MCN scenarios. -In subsequent labs, we'll explore security and observabilty concepts that build on MCN functionality. -Head over to the Network Connect exercise. - -
\ No newline at end of file diff --git a/labapp/app/markdown/lb.md b/labapp/app/markdown/lb.md index 01f9d3f..ddacf7b 100644 --- a/labapp/app/markdown/lb.md +++ b/labapp/app/markdown/lb.md @@ -20,19 +20,19 @@ Build an origin pool and load balancer based on the exercise requirements. @@ -107,15 +107,15 @@ Create a new origin pool for the Azure cloud app. Reuse your load balancer. @@ -146,5 +146,5 @@ document.getElementById('requestBtn2').addEventListener('click', () => { -Once you've completed both exercises, move on to the http routing exercise. +After completing both exercises, move on to the routing exercise. diff --git a/labapp/app/markdown/manipulation.md b/labapp/app/markdown/manipulation.md new file mode 100644 index 0000000..a5b9698 --- /dev/null +++ b/labapp/app/markdown/manipulation.md @@ -0,0 +1,112 @@ + + +# **Manipulation** + + + +Since web traffic has been traversing proxies, engineers have needed to alter HTTP content for increased observability ([XFF](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)), performance ([cache-control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)), or other reasons ([JWT](https://en.wikipedia.org/wiki/JSON_Web_Token)). +"Proxy Pass" functionality has been part of web servers since the early Apache days. +Adding, removing, and altering Headers are tablestakes for ADCs, CDNs, and software-based load balancers. +F5 XC App Connect enables this functionality granularly on routes or broadly on the load balancer. + + +
+ +### **Exercise 1: Path Rewrite** + +Configure a path prefix rewrite to remove part of the request path when routing to an origin. + + + +
+ +#### **Test Criteria** + +```http +GET https://eph-ns.mcn-lab.f5demos.com/aws/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com + +{ + "info": { + "path": "/raw" + } + ... +} +``` + +
+ +
+
+ + +Since questions on this functionality are often asked on [F5 DevCentral](https://community.f5.com/), a hint might be warranted. + +
+

+ + +

+
+
+ temp +
+
+
+ temp +
+
+
+
+ +
+ +### **Exercise 2: Header Shenanigans** + +Insert headers to identify + +
+ +#### **Test Criteria** + +```http +GET https://eph-ns.mcn-lab.f5demos.com/ HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com + +{ + "env": "azure", + ... +} +``` + +
+ +
+
+ + + + +Nice 🚀! If you've completed all the exercises so far, you have a good foundation for how App Connect addresses common L7 MCN scenarios. +In subsequent labs, we'll explore security and observabilty concepts that build on MCN functionality. +Head over to the Network Connect exercise. + diff --git a/labapp/app/markdown/overview.md b/labapp/app/markdown/overview.md index c518123..6f6e43f 100644 --- a/labapp/app/markdown/overview.md +++ b/labapp/app/markdown/overview.md @@ -1,52 +1,141 @@ -
-This lab is a "practical" training activity. -Each exercise will ask you to **configure** F5 Distributed Cloud ("XC") objects to reinforce core XC Multi-Cloud Networking ("MCN") concepts. -Once configured, you'll be asked to **test** your configuration using this web application. +# **Overview** -
- -# **Getting Started** -When your UDF deployment launched, two automated processes started - Customer Edge ("CE") registration and account provisioning in the [lab tenant](https://f5-xc-lab-mcn.console.ves.volterra.io/). +The lab environment, the application endpoints, and how you interact with the load balancer have been simplified in an effort to focus on concepts. +Understanding the environment, it's topology, and the rudimentary functionality of the cloud app will help in completing the exercises.
-## **Customer Edge** +## **Architecture** -The CE in your UDF deployment is being registered with the [lab tenant](https://f5-xc-lab-mcn.console.ves.volterra.io/). -CEs on first launch update software and, often, thier OS. This can be very time consuming. -This process will take 15-20 min from when the CE is booted. -You can still get started on some preliminary tasks while waiting. +The lab environment contains three distributed sites meshed using the F5 Distributed Cloud Global Network. - +
-**Note on status in nav and status page.** +
-## **Account Provisioning** +Arch diagram + +## **Cloud App** -Check the email used to launch your UDF deployment for a "welcome" or password reset email to the [lab tenant](https://f5-xc-lab-mcn.console.ves.volterra.io/). -Update your password and log into the tenant. +An instance of the cloud app is hosted in each remote cloud environment. +The cloud app is a simple application that echoes back an HTTP request. +While working through the lab, unless otherwise noted, the test results are displaying the headers and info **from the request received by the app**. + +For testing, you can access an endpoint of each cloud app from your browser.

- - - + + -

+ + +

+ + + +
+ +## **Lab Exercises** + +Lab exercises will ask you to create configuration in the lab tenant. +To complete a lab exercise, you will run a test against the load balancer advertised from the Customer Edge in your UDF site. +Tests are integrated in this lab app. + +
+ +#### **Test Criteria** + +Exercises will specify thier success criteria along with the test. + +Here are some examples to try. + +```http +GET https://foo.f5demos.com/ HTTP/1.1 + +{ + "info": "bar" +} +``` - +
diff --git a/labapp/app/templates/coming-soon.html b/labapp/app/templates/coming-soon.html index 28fc6e8..a9b1fa1 100644 --- a/labapp/app/templates/coming-soon.html +++ b/labapp/app/templates/coming-soon.html @@ -1,11 +1,11 @@ {% extends "base.html" %} -{% block title %}MCN Practical Error{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block content %}
- - Descriptive Text + + Coming Soon
{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/orig-base.html b/labapp/app/templates/orig-base.html new file mode 100644 index 0000000..712e3a0 --- /dev/null +++ b/labapp/app/templates/orig-base.html @@ -0,0 +1,221 @@ + + + + + {% block title %}{{ title }}{% endblock %} + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ {% block content %} + {% endblock %} +
+
+
+ + + + diff --git a/labapp/app/templates/setup.html b/labapp/app/templates/setup.html index cbf7e19..89805ef 100644 --- a/labapp/app/templates/setup.html +++ b/labapp/app/templates/setup.html @@ -5,8 +5,9 @@ {% block content %}
{{ content|safe }} +
-
+
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -14,11 +15,13 @@
{{ message }}
{% endfor %}
+
+ When ready, start the exercises. +
{% endif %} {% endwith %} -
-
+ {% endblock %} diff --git a/labapp/labapp_installer.sh b/labapp/labapp_installer.sh index a162229..a48634c 100644 --- a/labapp/labapp_installer.sh +++ b/labapp/labapp_installer.sh @@ -1,57 +1,57 @@ #!/bin/bash -# Check if Docker is installed, install it if it's not -if ! command -v pip &> /dev/null -then - # Update apt - sudo DEBIAN_FRONTEND=noninteractive apt-get update --yes - echo "pip could not be found, installing..." - sudo apt-get install -y python3-pip +# Ensure the script is run with root privileges +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit +fi + +# Check for Python3 pip and install if it's missing +if ! command -v pip3 &> /dev/null; then + echo "pip3 could not be found, updating repositories and installing..." + sudo apt-get update --quiet + sudo apt-get install --quiet --yes python3-pip fi # Variable Declarations -IMAGE=ghcr.io/f5devcentral/f5xc-lab-mcn-practical/labapp:latest -SERVICE=mcn-practical-labapp.service -APPDIR=/opt/mcn-practical-labapp/app -SCRIPTDIR=/opt/mcn-practical-labapp/script -REPO_URL=https://github.com/f5devcentral/f5xc-lab-mcn-practical.git -BRANCH=dev +REPO_URL="https://github.com/f5devcentral/f5xc-lab-mcn-practical.git" +BRANCH="main" +APPDIR="/opt/mcn-practical-labapp/app" +SCRIPTDIR="/opt/mcn-practical-labapp/scripts" -# Create directories -mkdir -p $SCRIPTDIR -mkdir -p $APPDIR +# Create necessary directories +mkdir -p "$APPDIR" "$SCRIPTDIR" -# Create the start_labapp.sh script -cat <$SCRIPTDIR/start_app.sh +# Create the start script +cat <"$SCRIPTDIR/start_app.sh" #!/bin/bash -if [ ! -d "$APPDIR/.git" ]; then - git clone -b $BRANCH $REPO_URL $APPDIR +# Navigate to the app directory +cd "$APPDIR" + +# Check if the directory is a git repository and if not, clone it +if [ ! -d ".git" ]; then + git clone --branch $BRANCH $REPO_URL . else - cd $APPDIR - # Ensure that the local repository is tracking the correct remote and branch + # Reset repository to match the remote repository git remote set-url origin $REPO_URL - git fetch --all - # Reset to the specified branch forcefully + git fetch --prune git checkout $BRANCH - git reset --hard origin/$BRANCH + git reset --hard "origin/$BRANCH" git clean -fdx - git pull origin $BRANCH fi # Install required Python packages -cd $APPDIR/labapp/app -pip install -r requirements.txt +pip3 install -r labapp/app/requirements.txt # Start the Gunicorn server -gunicorn --workers 4 --bind 0.0.0.0:1337 app:app +export UDF="true" && gunicorn --workers 4 --chdir labapp/app --bind 0.0.0.0:1337 app:app EOF -# Make the script executable -chmod +x $SCRIPTDIR/start_app.sh +chmod +x "$SCRIPTDIR/start_app.sh" # Create systemd service file -cat </etc/systemd/system/$SERVICE +cat <"/etc/systemd/system/mcn-practical-labapp.service" [Unit] Description=MCN Practical Lab App After=network.target @@ -60,14 +60,14 @@ After=network.target WorkingDirectory=$APPDIR ExecStart=/bin/bash $SCRIPTDIR/start_app.sh Restart=always +Type=simple [Install] WantedBy=multi-user.target EOF -# Reload systemd, enable and start the service +# Reload systemd to recognize new service, enable it systemctl daemon-reload -systemctl enable $SERVICE -systemctl start $SERVICE +systemctl enable mcn-practical-labapp.service -echo "$SERVICE has been installed and started as a systemd service." +echo "mcn-practical-labapp.service has been installed." diff --git a/labapp/service_install.sh b/labapp/service_install.sh deleted file mode 100644 index 6901895..0000000 --- a/labapp/service_install.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# Update apt -sudo DEBIAN_FRONTEND=noninteractive apt-get update --yes - -# Check if Docker is installed, install it if it's not -if ! command -v docker &> /dev/null -then - echo "Docker could not be found, installing..." - sudo apt-get install -y docker.io -fi - -# Enable and start Docker -sudo systemctl enable docker -sudo systemctl start docker - -# Variable Declarations -IMAGE=ghcr.io/f5devcentral/f5xc-lab-mcn-practical/labapp:latest -SERVICE=mcn-practical-labapp.service -CONTAINER=mcn-practical-labapp - -# Create the systemd service file -sudo bash -c "cat > /etc/systemd/system/$SERVICE </etc/systemd/resolved.conf.d/custom.conf +[Resolve] +DNSStubListener=no +DNS=127.0.0.1 +EOF + +# Restart systemd-resolved to apply changes +systemctl restart systemd-resolved + +# Install dnsmasq +apt-get update --quiet +apt-get install --quiet --yes dnsmasq + +# Configure dnsmasq to forward specific domain requests to given IP +cat </etc/dnsmasq.d/mcn-lab.conf +server=/mcn-lab.f5demos.com/10.1.1.4 +address=/ubuntu/127.0.0.1 +EOF + +# Restart dnsmasq to apply the new configuration +systemctl restart dnsmasq + +echo "Configuration complete."