+ F5 Distributed Cloud documentation blurb +
+ +diff --git a/labapp/app/app.py b/labapp/app/app.py index d62c698..234736c 100644 --- a/labapp/app/app.py +++ b/labapp/app/app.py @@ -3,12 +3,16 @@ """ import os import re +import json +import requests +import base64 +import urllib from flask import Flask, render_template, jsonify, request, redirect, make_response, flash, url_for from flask_caching import Cache -import requests import markdown from ce import get_ce_info, get_ce_state from fetch import get_runner_session, cloudapp_fetch, cloudapp_req_headers, cloudapp_res_headers +from score import score_get_results, score_build_table app = Flask(__name__) app.config['ce_info'] = None @@ -16,10 +20,13 @@ if os.getenv('UDF', None): app.config['ce_info'] = get_ce_info() app.config['UDF'] = True + app.config['SESSION_COOKIE_SECURE'] = True app.config['base_url'] = "mcn-lab.f5demos.com" app.config['CACHE_TYPE'] = 'SimpleCache' cache = Cache(app) app.secret_key = "blahblahblah" +data_cookie = "mcnp-ac-data" +cookie_age = 86400 session = get_runner_session() session.headers.update({"User-Agent": "MCN-Lab-Runner/1.0"}) @@ -42,20 +49,75 @@ def validate_eph_ns(input_name): pattern = r'^[a-zA-Z]+-[a-zA-Z]+$' return bool(re.match(pattern, input_name)) -def eph_ns() -> str: +def get_eph_ns() -> str: """check if ephemeral namespace is set""" - this_eph_ns = request.cookies.get('eph_ns', None) - return this_eph_ns + try: + cookie_b64 = request.cookies.get(data_cookie, None) + if cookie_b64: + return get_cookie_prop(cookie_b64, 'eph_ns') + except Exception: + print("Error getting ephemeral NS") + return None +def get_site() -> str: + """check if ephemeral namespace is set""" + if app.config['ce_info']: + return app.config['ce_info'].get("site_name", None) + return None + +def update_cookie_prop(cookie_b64, prop, value): + """Update a property in a base64 encoded JSON cookie.""" + try: + json_bytes = base64.b64decode(cookie_b64) + json_str = json_bytes.decode('utf-8') + cookie_data = json.loads(json_str) + if not isinstance(cookie_data, dict): + raise ValueError("Cookie data is not a dictionary.") + cookie_data[prop] = value + updated = json.dumps(cookie_data) + base64_bytes = base64.b64encode(updated.encode('utf-8')) + return base64_bytes.decode('utf-8') + except json.JSONDecodeError: + print("Error decoding JSON from cookie.") + """TBD: this is not what we want.""" + return encode_data({}) + except Exception as e: + print(f"An error occurred: {e}") + return encode_data({}) + +def get_cookie_prop(cookie_b64, prop): + """get a cookie prop""" + try: + json_bytes = base64.b64decode(cookie_b64) + json_str = json_bytes.decode('utf-8') + c_dict = json.loads(json_str) + return c_dict[prop] + except json.JSONDecodeError: + print("Error decoding cookie data") + return None + +def encode_data(data): + """Encode dictionary to Base64-encoded JSON.""" + json_str = json.dumps(data) + base64_bytes = base64.b64encode(json_str.encode('utf-8')) + return base64_bytes.decode('utf-8') + +def decode_data(encoded_data): + """Decode Base64-encoded JSON to dictionary.""" + json_bytes = base64.b64decode(encoded_data) + json_str = json_bytes.decode('utf-8') + return json.loads(json_str) + @app.errorhandler(404) @app.errorhandler(500) def return_err(err): """common error handler""" - img = { - 404: "/static/404.png", - 500: "/static/500.png" - } - return render_template("error.html", err_img=img[err.code]) + return render_template("error.html", code=err.code) + +@app.route('/cookie') +def cookie_err(): + """cookie error""" + return render_template("cookie.html") @app.after_request def cache_control(response): @@ -63,29 +125,36 @@ def cache_control(response): if request.path.startswith("/static/") and request.path.endswith(".png"): response.headers['Cache-Control'] = 'public, max-age=3600' return response + +@app.before_request +def ensure_cookie(): + """Ensure that the cookie is present, otherwise redirect to the cookie page.""" + if not request.path.startswith('/static'): + if (request.path not in ['/', '/cookie', '/_ce_status']) and (data_cookie not in request.cookies): + return redirect('/cookie') @app.route('/') def index(): """index page""" - html = render_md("markdown/welcome.md") - return render_template('standard.html', - title="MCN Practical: Overview", - content=html + html = render_template('welcome.html', + title="MCN Practical: Welcome" ) + response = make_response(html) + if data_cookie not in request.cookies: + response.set_cookie(data_cookie, encode_data({}), max_age=cookie_age) + return response @app.route('/overview') -def arch(): - """arch page""" - html = render_md("markdown/overview.md") - return render_template('standard.html', - title="MCN Practical: Architecture", - content=html +def overview(): + """overview page""" + return render_template('overview.html', + title="MCN Practical: Overview" ) @app.route('/setup', methods=['GET', 'POST']) def setup(): """setup page""" - ns = eph_ns() + ns = get_eph_ns() if request.method == 'POST': action = request.form['action'] if action == 'save': @@ -94,117 +163,114 @@ def setup(): flash("Invalid ephemeral namespace.", "danger") return redirect(url_for('setup')) response = make_response(redirect('/setup')) - response.set_cookie('eph_ns', this_eph_ns, max_age=60*60*24) + cookie_b64 = request.cookies.get('mcnp-ac-data', encode_data({})) + cookie_data = update_cookie_prop(cookie_b64, 'eph_ns', this_eph_ns) + response.set_cookie(data_cookie, cookie_data) flash('Ephemeral namespace successfully set.', "success") return response if action == 'clear': response = make_response(redirect('/setup')) - response.set_cookie('eph_ns', '', expires=0) + cookie_b64 = request.cookies.get('mcnp-ac-data', encode_data({})) + cookie_data = update_cookie_prop(cookie_b64, 'eph_ns', None) + response.set_cookie(data_cookie, cookie_data) flash("Ephemeral namespace cleared.", "info") return response - html = render_md("markdown/setup.md") return render_template('setup.html', title="MCN Practical: Setup", - content=html, ns=ns ) -@app.route('/_ce_status') -@cache.cached(timeout=30) -def ce_state(): - """get ce state (internal route)""" - data = get_ce_state(app.config['ce_info']) - return data - -@app.route('/lb') +@app.route('/loadbalancing') def lb(): """lb page""" - ns = eph_ns() - html = render_md("markdown/lb.md") - return render_template('exercise_standard.html', + ns = get_eph_ns() + site = get_site() + return render_template('loadbalancing.html', title="MCN Practical: LB", - content=html, + site=site, ns=ns ) @app.route('/route') def path(): """routing page""" - ns = eph_ns() - html = render_md("markdown/route.md") - return render_template('exercise_standard.html', + ns = get_eph_ns() + return render_template('route.html', title="MCN Practical: HTTP Routing", - content=html, - ns=ns, - + ns=ns ) @app.route('/manipulation') def header(): """manipulation page""" - ns = eph_ns() - html = render_md("markdown/manipulation.md") - return render_template('exercise_standard.html', + ns = get_eph_ns() + site = get_site() + return render_template('manipulation.html', title="MCN Practical: Manipulation", - content=html, - ns=ns + ns=ns, + site=site ) @app.route('/portability') def port(): """portability page""" - ns = eph_ns() - html = render_md("markdown/portability.md") - return render_template('exercise_standard.html', + ns = get_eph_ns() + return render_template('portability.html', title="MCN Practical: Portability", - content=html, - ns=ns - ) - -@app.route('/vnet') -def vnet(): - """vnet 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(): - """netpolicy 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') +@app.route('/reference') def ref(): """reference page""" - ns = eph_ns() - html = render_md("markdown/reference.md") + ns = get_eph_ns() return render_template('coming-soon.html', - title="MCN Practical: Reference", - content=html, - ns=ns + title="MCN Practical: Reference" ) @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, + try: + cookie_b64 = request.cookies.get(data_cookie) + this_score = get_cookie_prop(cookie_b64, 'score') + """raise a LabException""" + except Exception: + print("Error getting score") + this_score = {} + try: + p_score = score_get_results(this_score) + over_table = score_build_table(p_score, 'overview', 'Overview') + lb_table = score_build_table(p_score, 'lb', 'Load Balancing') + route_table = score_build_table(p_score, 'route', 'Routing') + manip_table = score_build_table(p_score, 'manip', 'Manipulation') + port_table = score_build_table(p_score, 'port', 'Portability') + except LabException as e: + print(f"Couldn't build score table: {e}") + return render_template('score.html', + title="MCN Practical: Scoreboard", + over_table=over_table, + lb_table=lb_table, + route_table=route_table, + manip_table=manip_table, + port_table=port_table, + ) + +@app.route('/test') +def test(): + """test page""" + ns = get_eph_ns() + return render_template('test.html', + title="MCN Practical: Test", ns=ns ) +@app.route('/_ce_status') +def ce_state(): + """get ce state (internal route)""" + data = get_ce_state(app.config['ce_info']) + return data + @app.route('/_test1') def ex_test(): """Example test""" @@ -234,7 +300,7 @@ def ex_test2(): def lb_aws(): """AWS LB test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}" @@ -249,7 +315,7 @@ def lb_aws(): def lb_azure(): """Azure LB test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}" @@ -269,7 +335,7 @@ def lb_azure(): def route1(): """First Route Test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] @@ -291,7 +357,7 @@ def route1(): def route2(): """First Route Test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] @@ -315,7 +381,7 @@ def route2(): def manip1(): """First Manip Test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] @@ -331,12 +397,13 @@ def manip1(): def manip2(): """Second Manip Test""" try: - ns = eph_ns() + ns = get_eph_ns() + site = get_site() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] url = f"https://{ns}.{base_url}/" - t_headers = { "x-mcn-namespace": ns, "x-mcn-src-site": app.config["ce_info"]["site_name"]} + t_headers = { "x-mcn-namespace": ns, "x-mcn-src-site": site} r_data = cloudapp_req_headers(session, url, 7, t_headers) return jsonify(status='success', data=r_data) except (LabException, ValueError) as e: @@ -348,7 +415,7 @@ def manip2(): def manip3(): """Third Manip Test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") base_url = app.config['base_url'] @@ -372,7 +439,7 @@ def manip3(): def port1(): """Friend test""" try: - ns = eph_ns() + ns = get_eph_ns() if not ns: raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}/" @@ -388,7 +455,6 @@ def port2(): """Friend test""" try: data = request.get_json() - print(data) eph_ns = data['userInput'] url = f"https://{eph_ns}.{app.config['base_url']}/" data = cloudapp_fetch(session, url, 7, 'info', {"method": "GET", "path": "/"}) diff --git a/labapp/app/markdown/lb.md b/labapp/app/markdown/lb.md deleted file mode 100644 index 96c0d4e..0000000 --- a/labapp/app/markdown/lb.md +++ /dev/null @@ -1,168 +0,0 @@ -
- Load Balancer Hint - - -
-- Route Hint - -
-- - - - -
- - - -### **While You Wait** - -Here's a few things you can do while waiting for the CE to be registered and provisioned: - -+ F5 Distributed Cloud documentation blurb +
+ +{name} | +
---|
${prettyJson}
${errJson}
${prettyJson}
${errJson}
${prettyJson}
${prettyJson}
${errJson}
${errJson}
+ Load balancing is the cornerstone of XC's App Connect functionality. + L7 MCN requires discovering services at one site and making those services available to another. + XC implements this functionality with origin pools and load balancers. + More complicated configurations (underlay networking, security services, observability) are built on these primitives. +
+ + + ++ For the initial exercise, make the cloud application running in AWS available to the UDF environment. + Build an origin pool and load balancer based on the exercise requirements. +
+ +
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "env": "AWS",
+ ...
+}
+
+
+
+
+
+
+
+
+
+
++ Since this is the first exercise, here are some hints. +
+ + ++ Load Balancer Hint + + +
++ For the second exercise, make the cloud application running in Azure available to the UDF environment. + Create a new origin pool for the Azure cloud app. Reuse your load balancer. +
+ + +
+GET https://{{ ns }}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "env": "Azure",
+ ...
+}
+
+
+
+
++Configure a path prefix rewrite to remove part of the request path when routing to an origin. +
+ +
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/aws/raw HTTP/1.1
+Host: eph-ns.mcn-lab.f5demos.com
+
+
+
+
+{
+ "info": {
+ "path": "/raw"
+ }
+ ...
+}
+
+
+
+
+
+
+
+
+
+ Questions on this functionality are often asked on F5 DevCentral. Here's a hint.
+
+
++ Route Hint + +
++ While blind header insertion or deletion is useful in some use cases, this exercise focuses on context aware header manipulation. + Use the XC Header Processing docs for reference. +
+ + + +
+GET https://{{ ns or 'eph-ns '}}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns or 'eph-ns '}}.mcn-lab.f5demos.com
+
+
+
+
+{
+ ...
+ "request_headers": {
+ "x-mcn-namespace": "wiggly-yellowtail",
+ "x-mcn-src-site": "{{ site or 'cluster-xxxxxxxxx' }}",
+ },
+ ...
+}
+
+
+
+
+
+
+
+
+
+
+
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/aws HTTP/1.1
+Host: {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "x-mcn-dest-site": "student-awsnet"
+}
+
+
+
+Request 2
+
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/azure HTTP/1.1
+Host: {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "x-mcn-dest-site": "student-azurenet"
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
++The lab environment, the service 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. +
+ + ++The lab environment contains three distributed sites meshed using the F5 Distributed Cloud Global Network. +
+ ++Lab exercises will ask you to create configuration in the lab tenant. +Exercise reqirements are listed in a table along with an object type indicator. +
+ ++To complete lab exercises, you will run tests against the load balancer advertised from the Customer Edge in your UDF site. +You will build this load balancer in the first exercise. +All tests will be run from this web apllication. +
++Each test will specify success criteria followed by a button. +
++Here are some illustrative examples. +
+ ++The first section of the test criteria shows the request being made. +
+ +
+GET https://foo.mcn-lab.f5demos.com/ HTTP/1.1
+Host: foo.mcn-lab.f5demos.com
+
+
+
++The second section shows a value the test expects in the response. +
+ +
+{
+ "info": {
+ "foo": True
+ }
+}
+
+
+
+
+
+
+
+
+ The test made a request to https://foo.mcn-lab.f5demos.com.
+ The test succeeded because the response contained the JSON
string {"info": { "foo": True }}
.
+
+GET https://foo.mcn-lab.f5demos.com/ HTTP/1.1
+Host: foo.mcn-lab.f5demos.com
+
+
+
+
+{
+ "info": {
+ "bar": True
+ }
+}
+
+
+
+
+
+
+
+
+The test made a request to https://foo.mcn-lab.f5demos.com.
+The test failed because the response did not contain the JSON
string { "info": { "bar": True}}
.
+
curl
and jq
are provided on the UDF Runner instance.
+ ubuntu@ubuntu:~$ curl -s https://foo.mcn-lab.f5demos.com/ | jq
+ {
+ "info": {
+ "foo": true
+ }
+ }
+
+
++Exercises are run against instances of the cloud app hosted in each remote cloud environment. +The cloud app simply echoes back HTTP request info. +Unless otherwise noted, test results display headers and info from the request received by the app. +In other words, test critera are evaluating being sent (as echoed back from the cloud app). + +To demonstrate, you can access an endpoint of each cloud app from your browser. +
+ + + + + ++Use the lab repository issue tracker to report bugs, typos, or lab enhancements. +
+ + + ++ The configuration built so far handles load balancing, routing, and content manipulation. + XC refers to this object as a "load balancer" but it's really the holistic representation of an application whose service endpoints live across the distributed network. + The object is simple - it doesn't yet include configuration for WAAP, API protection, or a service policy. +
++ A key advantage of XC over traditional ADCs is its flexibility in specifying where a load balancer is advertised. +
+ + + +
+GET https://{{ ns or 'eph-ns '}}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns or 'eph-ns '}}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "info": {
+ "path": "/"
+ }
+ ...
+}
+
+
+
+
+
+
+
++ Do you have a friend working on the lab? + Have they updated their advertise policy to use the virtual site? + Find their ephemeral namespace (or use the one provided in the form). +
+ +
+GET https://wiggly-yellowtail.mcn-lab.f5demos.com/ HTTP/1.1
+Host: wiggly-yellowtail.mcn-lab.f5demos.com
+
+
+
+
+{
+ "info": {
+ "path": "/"
+ }
+ ...
+}
+
+
+
++Modern applications, and some classic ones, are often comprised of disparate services spread across sites. +MCN solutions must be able to make routing decisions based on characterstics of an HTTP request. +F5 XC App Connect is a distributed L7 proxy that provides intelligent routing, visibility, and strategic points of control. +
+ + + ++Build routing rules and configure your load balancer to route traffic between the two cloud apps based on the request url. +
+ +
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/aws/raw HTTP/1.1
+Host: {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "env": "aws",
+ ...
+}
+
+
+
+Request 2
+
+GET https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/azure/raw HTTP/1.1
+Host: {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com
+
+
+
+
+{
+ "env": "azure",
+ ...
+}
+
+
+
+
+
+
+
+
+
+
+
++Build rules to route traffic between the two cloud apps based on an arbitrary HTTP request header. +
+ +
+GET https://{{ ns or 'eph-ns '}}.mcn-lab.f5demos.com/raw HTTP/1.1
+Host: {{ ns or 'eph-ns '}}.mcn-lab.f5demos.com
+X-MCN-lab: aws
+
+
+
+
+{
+ "env": "aws",
+ ...
+}
+
+
+
+Request 2
+
+GET https://{{ ns or 'eph-ns '}}.mcn-lab.f5demos.com/raw HTTP/1.1
+Host: {{ ns or 'eph-ns '}}.mcn-lab.f5demos.com
+X-MCN-lab: azure
+
+
+
+
+{
+ "env": "azure",
+ ...
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
++ + Please tell us your thoughts about the lab using this form. +
+ ++ Use the lab repository issue tracker to report bugs, typos, or lab enhancements. +
++ Log in to the lab tenant and open any namespaced tile - Multi-Cloud App Connect, Distributed Apps, etc. + The ephemeral namespace is a randomly generated concatenation of adjective-animal in the navigation bar towards the top. +
+ + + ++ The ephemeral namespace will be used to derive a unique URL for the load balancer used in the lab exercises. +
+ + + + ++ When your UDF deployment launched, two automated processes started - Customer Edge ("CE") registration and account provisioning in the lab tenant. +
+ + + ++ The CE in the UDF deployment will register with the lab tenant. + CEs on first launch update software and, often, their OS. This can take ~20 min from when the CE is booted. +
+ ++ When the CE is ready, the status indicator in the navigation pane (👀 look to the left) will show . + Use the indicator to find the CE site name needed for configuring the load balancer's advertise policy. +
+ + ++ Check the email used to launch your UDF deployment for a welcome or password reset email from the lab tenant. + Update your password to log into the tenant. +
+ + + + + +