diff --git a/labapp/app/app.py b/labapp/app/app.py index 752b22b..1b8f18a 100644 --- a/labapp/app/app.py +++ b/labapp/app/app.py @@ -5,6 +5,7 @@ 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 @@ -19,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"}) @@ -47,18 +51,73 @@ def validate_eph_ns(input_name): 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): @@ -66,23 +125,30 @@ 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']) 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']) @@ -97,36 +163,31 @@ 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('/loadbalancing') def lb(): """lb page""" ns = get_eph_ns() - html = render_md("markdown/lb.md") - return render_template('exercise_standard.html', + site = get_site() + return render_template('loadbalancing.html', title="MCN Practical: LB", - content=html, + site=site, ns=ns ) @@ -134,22 +195,17 @@ def lb(): def path(): """routing page""" ns = get_eph_ns() - html = render_md("markdown/route.md") - return render_template('exercise_standard.html', + return render_template('route.html', title="MCN Practical: HTTP Routing", - content=html, - ns=ns, - + ns=ns ) @app.route('/manipulation') def header(): """manipulation page""" ns = get_eph_ns() - html = render_md("markdown/manipulation.md") - return render_template('exercise_standard.html', + return render_template('manipulation.html', title="MCN Practical: Manipulation", - content=html, ns=ns ) @@ -157,32 +213,8 @@ def header(): def port(): """portability page""" ns = get_eph_ns() - html = render_md("markdown/portability.md") - return render_template('exercise_standard.html', + return render_template('portability.html', title="MCN Practical: Portability", - content=html, - ns=ns - ) - -@app.route('/vnet') -def vnet(): - """vnet page""" - ns = get_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 = get_eph_ns() - html = render_md("markdown/reference.md") - return render_template('coming-soon.html', - title="MCN Practical: Reference", - content=html, ns=ns ) @@ -200,14 +232,12 @@ def ref(): @app.route('/score') def score(): """scoreboard page""" - ns = get_eph_ns() - score_cookie = request.cookies.get('score', '%7B%7D') - print(score_cookie) try: - decoded_cookie = urllib.parse.unquote(score_cookie) - enc_score = json.loads(decoded_cookie) - this_score = {urllib.parse.unquote(k): v for k, v in enc_score.items()} - except json.JSONDecodeError: + 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) @@ -218,19 +248,29 @@ def score(): port_table = score_build_table(p_score, 'port', 'Portability') except LabException as e: print(f"Couldn't build score table: {e}") - response = make_response(render_template('score.html', + 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, - ns=ns - )) - response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = '0' - return response + ) + +@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(): @@ -359,11 +399,12 @@ def manip2(): """Second Manip Test""" try: 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: @@ -415,7 +456,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/score.py b/labapp/app/score.py index 1be4b08..085f189 100644 --- a/labapp/app/score.py +++ b/labapp/app/score.py @@ -40,7 +40,7 @@ "href": "/route", "result": "none" }, - "/_manip1:": { + "/_manip1": { "name": "Path Rewrite", "key": "manip", "href": "/manipulation", @@ -86,8 +86,6 @@ def score_get_results(cookie_results): for test_path, result in cookie_results.items(): if test_path in score_schema: this_score[test_path]['result'] = result - print(this_score) - print(score_schema) return this_score def score_sort(scores, key): @@ -100,7 +98,6 @@ def score_build_table(scores, section, name): section_scores = score_sort(scores, section) rows_html = "" for key, score in section_scores.items(): - print(score['result']) r_icon = result_map[score['result']] section_html = f""" diff --git a/labapp/app/static/arch.png b/labapp/app/static/arch.png deleted file mode 100644 index 8c8e283..0000000 Binary files a/labapp/app/static/arch.png and /dev/null differ diff --git a/labapp/app/static/custom.css b/labapp/app/static/custom.css index f92034d..61c29cf 100644 --- a/labapp/app/static/custom.css +++ b/labapp/app/static/custom.css @@ -18,6 +18,58 @@ main { overflow-y: auto; } +.lab-icon { + height: 30px; + width: auto; + padding-right: 25px; +} + +.lab-test-button { + width: 175px; +} + +.lab-post-button { + width: 200px; +} + +.lab-ex-test { + width: 125px; +} + +.lab-image-container { + height: 200px; /* Fixed height */ + overflow: hidden; /* Ensures nothing extends outside the container */ + display: flex; /* Flexbox to help with centering/positioning the image */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically, if needed */ +} + +.lab-header { + border-radius: .8em; + filter: brightness(110%) contrast(80%); + filter: blur(.1px); +} + +.lab-header-clear { + border-radius: .8em; +} +/* Testing */ + +.sidebar-link { + display: block; + padding: 8px 16px; + color: #333; /* Adjust the color to fit your theme */ + text-decoration: none; /* Remove underline from links */ + border-radius: 4px; /* Rounded corners for links */ +} + +.sidebar-link:hover { + background-color: #f8f9fa; /* Light grey background on hover */ + color: #0056b3; /* Change color on hover for better visibility */ +} + +/* Testing */ + .row { height: 100%; } @@ -47,18 +99,18 @@ main { padding: 50px; } -.err { +.err-container { display: flex; - justify-content: center; /* Centers horizontally */ - align-items: center; /* Centers vertically */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ height: 100vh; /* Full viewport height */ + margin-bottom: 0 } - -.err img { - display: block; - width: 100%; /* This will make the image responsive */ - max-width: 600px; /* Maximum width of the image */ - margin: 0 auto; /* Center the image */ +.err-container img { + width: 80%; + max-width: 100%; + height: auto; /* Maintain aspect ratio */ + border-radius: .8em; } .err p { diff --git a/labapp/app/static/dracula.css b/labapp/app/static/dracula.css deleted file mode 100644 index 087149f..0000000 --- a/labapp/app/static/dracula.css +++ /dev/null @@ -1,81 +0,0 @@ -/* Dracula Theme v1.2.5 - * - * https://github.com/dracula/highlightjs - * - * Copyright 2016-present, All rights reserved - * - * Code licensed under the MIT license - * - * @author Denis Ciccale - * @author Zeno Rocha - */ - -.hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - background: #282a36; -} - -.hljs-built_in, -.hljs-selector-tag, -.hljs-section, -.hljs-link { - color: #8be9fd; -} - -.hljs-keyword { - color: #ff79c6; -} - -.hljs, -.hljs-subst { - color: #f8f8f2; -} - -.hljs-title, -.hljs-attr, -.hljs-meta-keyword { - font-style: italic; - color: #50fa7b; -} - -.hljs-string, -.hljs-meta, -.hljs-name, -.hljs-type, -.hljs-symbol, -.hljs-bullet, -.hljs-addition, -.hljs-variable, -.hljs-template-tag, -.hljs-template-variable { - color: #f1fa8c; -} - -.hljs-comment, -.hljs-quote, -.hljs-deletion { - color: #6272a4; -} - -.hljs-keyword, -.hljs-selector-tag, -.hljs-literal, -.hljs-title, -.hljs-section, -.hljs-doctag, -.hljs-type, -.hljs-name, -.hljs-strong { - font-weight: bold; -} - -.hljs-literal, -.hljs-number { - color: #bd93f9; -} - -.hljs-emphasis { - font-style: italic; -} diff --git a/labapp/app/static/helpers-old.js b/labapp/app/static/helpers-old.js new file mode 100644 index 0000000..8bcf234 --- /dev/null +++ b/labapp/app/static/helpers-old.js @@ -0,0 +1,127 @@ +function setCookie(name, value, days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} + +function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +async function testHttpRequest(buttonId, requestUrl, resultDivId, buttonTxt) { + const button = document.getElementById(buttonId); + const resultDiv = document.getElementById(resultDivId); + + // Add spinner to button and disable it + button.innerHTML = `Testing...`; + button.disabled = true; + + try { + const response = await axios.get(requestUrl); + if (response.data.status === 'success') { + const prettyJson = JSON.stringify(response.data.data, null, 4); + resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; + updateScoreCookie(requestUrl, 'pass'); + } else { + const errJson = JSON.stringify(response.data.error, null, 4); + resultDiv.innerHTML = `
Request Failed:
${errJson}
`; + updateScoreCookie(requestUrl, 'fail'); + } + } catch (error) { + resultDiv.innerHTML = `
Error: ${error.message}
`; + updateScoreCookie(requestUrl, 'fail'); + } finally { + // Restore original button text and remove spinner + button.innerHTML = buttonTxt; + button.disabled = false; + resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +} + +async function testPostRequest(buttonId, requestUrl, resultDivId, inputDataId, buttonTxt) { + const button = document.getElementById(buttonId); + const resultDiv = document.getElementById(resultDivId); + const inputData = document.getElementById(inputDataId).value; + + + // Add spinner and change button text + button.innerHTML = `Testing...`; + button.disabled = true; + + try { + const response = await axios.post(requestUrl, { userInput: inputData }); + if (response.data.status === 'success') { + const prettyJson = JSON.stringify(response.data.data, null, 4); + resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; + updateScoreCookie(requestUrl, 'pass'); + } else { + const errJson = JSON.stringify(response.data.error, null, 4); + resultDiv.innerHTML = `
Request Failed:
${errJson}
`; + updateScoreCookie(requestUrl, 'fail'); + } + } catch (error) { + resultDiv.innerHTML = `
Error: ${error.message}
`; + updateScoreCookie(requestUrl, 'fail'); + } finally { + // Restore original button text + button.innerHTML = buttonTxt; + button.disabled = false; + resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +} + + function updateScoreCookie_old(requestUrl, status) { + // Get the current cookie, decode it, and parse it as JSON + const currentCookie = decodeURIComponent(getScoreCookie('mcnp_scoreboard') || '%7B%7D'); // Ensure the default value is an encoded empty JSON object + let progress = JSON.parse(currentCookie); + progress[encodeURIComponent(requestUrl)] = status; + document.cookie = `mcnp_scoreboard=${encodeURIComponent(JSON.stringify(progress))}; path=/; expires=${new Date(new Date().getTime() + 86400e3).toUTCString()};`; + } + + function getScoreCookie(name) { + let cookieArray = document.cookie.split(';'); + for(let i = 0; i < cookieArray.length; i++) { + let cookiePair = cookieArray[i].split('='); + if(name == cookiePair[0].trim()) { + return cookiePair[1]; + } + } + return null; + } + + function updateScoreCookie(requestUrl, status) { + // Retrieve the existing score cookie or initialize it to an empty JSON object. + // The cookie value retrieved is assumed to be URI-encoded since it was set as such. + let currentCookie = getCookie('mcnp_scoreboard') || '{}'; + + // Decode the retrieved cookie string to handle any URI-encoded characters. + let decodedCookie = decodeURIComponent(currentCookie); + + // Parse the decoded cookie string into an object. + let progress = JSON.parse(decodedCookie); + + // Update the progress object with the new status for the requestUrl. + // Using encodeURIComponent to ensure the URL is safely encoded. + progress[encodeURIComponent(requestUrl)] = status; + + // Serialize the progress object back into a JSON string. + let jsonString = JSON.stringify(progress); + + // Encode the entire JSON string to ensure all special characters are URI-safe. + let encodedJsonString = encodeURIComponent(jsonString); + + // Use the setCookie function to update the cookie with the URI-encoded JSON string. + setCookie('mcnp_scoreboard', encodedJsonString, 1); // Set for 1 day expiration +} + diff --git a/labapp/app/static/helpers.js b/labapp/app/static/helpers.js index 18e50b4..3c0cc14 100644 --- a/labapp/app/static/helpers.js +++ b/labapp/app/static/helpers.js @@ -1,88 +1,119 @@ +// Utility function to get a cookie by name +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +} + +// Utility function to set a cookie function setCookie(name, value, days) { - var expires = ""; + let expires = ''; if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days*24*60*60*1000)); - expires = "; expires=" + date.toUTCString(); + const date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = `; expires=${date.toUTCString()}`; } - document.cookie = name + "=" + (value || "") + expires + "; path=/"; + document.cookie = `${name}=${value}${expires}; path=/`; } -function getCookie(name) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for(var i=0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0)==' ') c = c.substring(1,c.length); - if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); +function updateScore(requestUrl, status) { + // Retrieve the current cookie, assume it's base64 encoded, or default to an encoded empty object + const base64EncodedData = getCookie('mcnp-ac-data') || btoa('{}'); + const cookieStr = atob(base64EncodedData); + let cookieData = JSON.parse(cookieStr); + + // Check if the 'score' object exists, if not initialize it + if (!cookieData.score) { + cookieData.score = {}; } - return null; + + // Update the score object with the result of the current request + cookieData.score[requestUrl] = status; + + // Convert the updated cookie object back to string, then encode to base64 + const updatedStr = JSON.stringify(cookieData); + const updatedBase64Data = btoa(updatedStr); + + // Update the cookie with the new base64 encoded data + setCookie('mcnp-ac-data', updatedBase64Data, 1); +} + +function clearScore(requestUrl, status) { + // Retrieve the current cookie, assume it's base64 encoded, or default to an encoded empty object + const base64EncodedData = getCookie('mcnp-ac-data') || btoa('{}'); + const cookieStr = atob(base64EncodedData); + let cookieData = JSON.parse(cookieStr); + + // Clear the score + cookieData.score = {}; + + // Convert the updated cookie object back to string, then encode to base64 + const updatedStr = JSON.stringify(cookieData); + const updatedBase64Data = btoa(updatedStr); + + // Update the cookie with the new base64 encoded data + setCookie('mcnp-ac-data', updatedBase64Data, 1); } -async function testHttpRequest(buttonId, requestUrl, resultDivId) { +async function testHttpRequest(buttonId, requestUrl, resultDivId, buttonTxt) { const button = document.getElementById(buttonId); const resultDiv = document.getElementById(resultDivId); + + // Add spinner to button and disable it + button.innerHTML = `Testing...`; button.disabled = true; + try { const response = await axios.get(requestUrl); if (response.data.status === 'success') { const prettyJson = JSON.stringify(response.data.data, null, 4); - resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; - updateScoreCookie(requestUrl, 'pass'); + resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; + updateScore(requestUrl, 'pass'); } else { const errJson = JSON.stringify(response.data.error, null, 4); - resultDiv.innerHTML = `
Request Failed:
${errJson}
`; - updateScoreCookie(requestUrl, 'fail'); + resultDiv.innerHTML = `
Request Failed:
${errJson}
`; + updateScore(requestUrl, 'fail'); } } catch (error) { resultDiv.innerHTML = `
Error: ${error.message}
`; - updateScoreCookie(requestUrl, 'fail'); + updateScore(requestUrl, 'fail'); } finally { + // Restore original button text and remove spinner + button.innerHTML = buttonTxt; button.disabled = false; resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } - } +} - async function testPostRequest(buttonId, requestUrl, resultDivId, inputDataId) { +async function testPostRequest(buttonId, requestUrl, resultDivId, inputDataId, buttonTxt) { const button = document.getElementById(buttonId); const resultDiv = document.getElementById(resultDivId); const inputData = document.getElementById(inputDataId).value; + + + // Add spinner and change button text + button.innerHTML = `Testing...`; button.disabled = true; + try { const response = await axios.post(requestUrl, { userInput: inputData }); if (response.data.status === 'success') { const prettyJson = JSON.stringify(response.data.data, null, 4); - resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; - updateScoreCookie(requestUrl, 'pass'); + resultDiv.innerHTML = `
Request Succeeded:
${prettyJson}
`; + updateScore(requestUrl, 'pass'); } else { const errJson = JSON.stringify(response.data.error, null, 4); - resultDiv.innerHTML = `
Request Failed:
${errJson}
`; - updateScoreCookie(requestUrl, 'fail'); + resultDiv.innerHTML = `
Request Failed:
${errJson}
`; + updateScore(requestUrl, 'fail'); } } catch (error) { resultDiv.innerHTML = `
Error: ${error.message}
`; - updateScoreCookie(requestUrl, 'fail'); + updateScore(requestUrl, 'fail'); } finally { + // Restore original button text + button.innerHTML = buttonTxt; button.disabled = false; resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } - } - - function updateScoreCookie(requestUrl, status) { - // Get the current cookie, decode it, and parse it as JSON - const currentCookie = decodeURIComponent(getScoreCookie('score') || '%7B%7D'); // Ensure the default value is an encoded empty JSON object - let progress = JSON.parse(currentCookie); - progress[encodeURIComponent(requestUrl)] = status; - document.cookie = `score=${encodeURIComponent(JSON.stringify(progress))}; path=/; expires=${new Date(new Date().getTime() + 86400e3).toUTCString()};`; - } - - function getScoreCookie(name) { - let cookieArray = document.cookie.split(';'); - for(let i = 0; i < cookieArray.length; i++) { - let cookiePair = cookieArray[i].split('='); - if(name == cookiePair[0].trim()) { - return cookiePair[1]; - } - } - return null; - } +} \ No newline at end of file diff --git a/labapp/app/static/images/404-err.png b/labapp/app/static/images/404-err.png new file mode 100644 index 0000000..4b3e875 Binary files /dev/null and b/labapp/app/static/images/404-err.png differ diff --git a/labapp/app/static/images/500-err.png b/labapp/app/static/images/500-err.png new file mode 100644 index 0000000..9ec8cc9 Binary files /dev/null and b/labapp/app/static/images/500-err.png differ diff --git a/labapp/app/static/images/cookie-err.png b/labapp/app/static/images/cookie-err.png new file mode 100644 index 0000000..ad188e7 Binary files /dev/null and b/labapp/app/static/images/cookie-err.png differ diff --git a/labapp/app/static/images/lb-banner.png b/labapp/app/static/images/lb-banner.png new file mode 100644 index 0000000..8ef6194 Binary files /dev/null and b/labapp/app/static/images/lb-banner.png differ diff --git a/labapp/app/static/images/manip-banner.png b/labapp/app/static/images/manip-banner.png new file mode 100644 index 0000000..8d4143e Binary files /dev/null and b/labapp/app/static/images/manip-banner.png differ diff --git a/labapp/app/static/images/overview-banner.png b/labapp/app/static/images/overview-banner.png new file mode 100644 index 0000000..eb2f68b Binary files /dev/null and b/labapp/app/static/images/overview-banner.png differ diff --git a/labapp/app/static/images/port-banner.png b/labapp/app/static/images/port-banner.png new file mode 100644 index 0000000..c14fefc Binary files /dev/null and b/labapp/app/static/images/port-banner.png differ diff --git a/labapp/app/static/practical.png b/labapp/app/static/images/practical.png similarity index 100% rename from labapp/app/static/practical.png rename to labapp/app/static/images/practical.png diff --git a/labapp/app/static/images/routing-banner.png b/labapp/app/static/images/routing-banner.png new file mode 100644 index 0000000..0ee6cfa Binary files /dev/null and b/labapp/app/static/images/routing-banner.png differ diff --git a/labapp/app/static/images/score-banner.png b/labapp/app/static/images/score-banner.png new file mode 100644 index 0000000..8daf037 Binary files /dev/null and b/labapp/app/static/images/score-banner.png differ diff --git a/labapp/app/static/images/setup-banner.png b/labapp/app/static/images/setup-banner.png new file mode 100644 index 0000000..7642d7c Binary files /dev/null and b/labapp/app/static/images/setup-banner.png differ diff --git a/labapp/app/static/load-balancing.png b/labapp/app/static/load-balancing.png deleted file mode 100644 index 1fd7284..0000000 Binary files a/labapp/app/static/load-balancing.png and /dev/null differ diff --git a/labapp/app/static/logo.png b/labapp/app/static/logo.png deleted file mode 100644 index e9a66db..0000000 Binary files a/labapp/app/static/logo.png and /dev/null differ diff --git a/labapp/app/static/manip.png b/labapp/app/static/manip.png deleted file mode 100644 index 772b24a..0000000 Binary files a/labapp/app/static/manip.png and /dev/null differ diff --git a/labapp/app/static/ms-forms.png b/labapp/app/static/ms-forms.png new file mode 100644 index 0000000..1164aec Binary files /dev/null and b/labapp/app/static/ms-forms.png differ diff --git a/labapp/app/static/network-policy.png b/labapp/app/static/network-policy.png deleted file mode 100644 index d1d0ddc..0000000 Binary files a/labapp/app/static/network-policy.png and /dev/null differ diff --git a/labapp/app/static/new_improved_logo.png b/labapp/app/static/new_improved_logo.png deleted file mode 100644 index a3c00cd..0000000 Binary files a/labapp/app/static/new_improved_logo.png and /dev/null differ diff --git a/labapp/app/static/nord.css b/labapp/app/static/nord.css new file mode 100644 index 0000000..88b406a --- /dev/null +++ b/labapp/app/static/nord.css @@ -0,0 +1,315 @@ +/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +title Nord highlight.js + +project nord-highlightjs + +version 0.1.0 + +repository https://github.com/arcticicestudio/nord-highlightjs + +author Arctic Ice Studio + +email development@arcticicestudio.com + +copyright Copyright (C) 2017 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +[References] +Nord + https://github.com/arcticicestudio/nord +highlight.js + http://highlightjs.readthedocs.io/en/latest/style-guide.html + http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html +*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #2E3440; +} + +.hljs, +.hljs-subst { + color: #D8DEE9; +} + +.hljs-selector-tag { + color: #81A1C1; +} + +.hljs-selector-id { + color: #8FBCBB; + font-weight: bold; +} + +.hljs-selector-class { + color: #8FBCBB; +} + +.hljs-selector-attr { + color: #8FBCBB; +} + +.hljs-selector-pseudo { + color: #88C0D0; +} + +.hljs-addition { + background-color: rgba(163, 190, 140, 0.5); +} + +.hljs-deletion { + background-color: rgba(191, 97, 106, 0.5); +} + +.hljs-built_in, +.hljs-type { + color: #8FBCBB; +} + +.hljs-class { + color: #8FBCBB; +} + +.hljs-function { + color: #88C0D0; +} + +.hljs-function > .hljs-title { + color: #88C0D0; +} + +.hljs-keyword, +.hljs-literal, +.hljs-symbol { + color: #81A1C1; +} + +.hljs-number { + color: #B48EAD; +} + +.hljs-regexp { + color: #EBCB8B; +} + +.hljs-string { + color: #A3BE8C; +} + +.hljs-title { + color: #8FBCBB; +} + +.hljs-params { + color: #D8DEE9; +} + +.hljs-bullet { + color: #81A1C1; +} + +.hljs-code { + color: #8FBCBB; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-formula { + color: #8FBCBB; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link:hover { + text-decoration: underline; +} + +.hljs-quote { + color: #4C566A; +} + +.hljs-comment { + color: #4C566A; +} + +.hljs-doctag { + color: #8FBCBB; +} + +.hljs-meta, +.hljs-meta-keyword { + color: #5E81AC; +} + +.hljs-meta-string { + color: #A3BE8C; +} + +.hljs-attr { + color: #8FBCBB; +} + +.hljs-attribute { + color: #D8DEE9; +} + +.hljs-builtin-name { + color: #81A1C1; +} + +.hljs-name { + color: #81A1C1; +} + +.hljs-section { + color: #88C0D0; +} + +.hljs-tag { + color: #81A1C1; +} + +.hljs-variable { + color: #D8DEE9; +} + +.hljs-template-variable { + color: #D8DEE9; +} + +.hljs-template-tag { + color: #5E81AC; +} + +.abnf .hljs-attribute { + color: #88C0D0; +} + +.abnf .hljs-symbol { + color: #EBCB8B; +} + +.apache .hljs-attribute { + color: #88C0D0; +} + +.apache .hljs-section { + color: #81A1C1; +} + +.arduino .hljs-built_in { + color: #88C0D0; +} + +.aspectj .hljs-meta { + color: #D08770; +} + +.aspectj > .hljs-title { + color: #88C0D0; +} + +.bnf .hljs-attribute { + color: #8FBCBB; +} + +.clojure .hljs-name { + color: #88C0D0; +} + +.clojure .hljs-symbol { + color: #EBCB8B; +} + +.coq .hljs-built_in { + color: #88C0D0; +} + +.cpp .hljs-meta-string { + color: #8FBCBB; +} + +.css .hljs-built_in { + color: #88C0D0; +} + +.css .hljs-keyword { + color: #D08770; +} + +.diff .hljs-meta { + color: #8FBCBB; +} + +.ebnf .hljs-attribute { + color: #8FBCBB; +} + +.glsl .hljs-built_in { + color: #88C0D0; +} + +.groovy .hljs-meta:not(:first-child) { + color: #D08770; +} + +.haxe .hljs-meta { + color: #D08770; +} + +.java .hljs-meta { + color: #D08770; +} + +.ldif .hljs-attribute { + color: #8FBCBB; +} + +.lisp .hljs-name { + color: #88C0D0; +} + +.lua .hljs-built_in { + color: #88C0D0; +} + +.moonscript .hljs-built_in { + color: #88C0D0; +} + +.nginx .hljs-attribute { + color: #88C0D0; +} + +.nginx .hljs-section { + color: #5E81AC; +} + +.pf .hljs-built_in { + color: #88C0D0; +} + +.processing .hljs-built_in { + color: #88C0D0; +} + +.scss .hljs-keyword { + color: #81A1C1; +} + +.stylus .hljs-keyword { + color: #81A1C1; +} + +.swift .hljs-meta { + color: #D08770; +} + +.vim .hljs-built_in { + color: #88C0D0; + font-style: italic; +} + +.yaml .hljs-meta { + color: #D08770; +} \ No newline at end of file diff --git a/labapp/app/static/path.png b/labapp/app/static/path.png deleted file mode 100644 index b26d612..0000000 Binary files a/labapp/app/static/path.png and /dev/null differ diff --git a/labapp/app/static/portable.png b/labapp/app/static/portable.png deleted file mode 100644 index a8af9ca..0000000 Binary files a/labapp/app/static/portable.png and /dev/null differ diff --git a/labapp/app/static/setup.png b/labapp/app/static/setup.png deleted file mode 100644 index 87576c5..0000000 Binary files a/labapp/app/static/setup.png and /dev/null differ diff --git a/labapp/app/static/table.css b/labapp/app/static/table.css index c05fec5..a5983ec 100644 --- a/labapp/app/static/table.css +++ b/labapp/app/static/table.css @@ -20,7 +20,7 @@ } .score-table td { - background: rgb(230, 234, 253); + background: rgb(215, 223, 223); color: black; } diff --git a/labapp/app/static/tls-icon.png b/labapp/app/static/tls-icon.png deleted file mode 100644 index 51e2b30..0000000 Binary files a/labapp/app/static/tls-icon.png and /dev/null differ diff --git a/labapp/app/static/virtual-networks.png b/labapp/app/static/virtual-networks.png deleted file mode 100644 index 2d60427..0000000 Binary files a/labapp/app/static/virtual-networks.png and /dev/null differ diff --git a/labapp/app/templates/base.html b/labapp/app/templates/base.html index 9446736..e478ad4 100644 --- a/labapp/app/templates/base.html +++ b/labapp/app/templates/base.html @@ -10,7 +10,7 @@ - + @@ -22,6 +22,10 @@
+ + + + + + + {% if config['UDF'] %} + +
+

{{ config["ce_info"]["site_name"] }}

+ Status +
+ {% endif %} + +
+
+ + +
{% block content %} diff --git a/labapp/app/templates/cookie.html b/labapp/app/templates/cookie.html new file mode 100644 index 0000000..36a7c5c --- /dev/null +++ b/labapp/app/templates/cookie.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+
+ Banner + +   Home +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/error.html b/labapp/app/templates/error.html index 284215c..472f608 100644 --- a/labapp/app/templates/error.html +++ b/labapp/app/templates/error.html @@ -3,9 +3,29 @@ {% block title %}MCN Practical Error{% endblock %} {% block content %} -
- - Descriptive Text - + {% if code == 404 %} +
+
+ Banner +
+ {% endif %} + + {% if code == 500 %} + + + +
+
+ Banner + +
+
+ {% endif %} + {% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/future_base.html b/labapp/app/templates/future_base.html deleted file mode 100644 index e0b9a83..0000000 --- a/labapp/app/templates/future_base.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - {% block title %}{{ title }}{% endblock %} - - - - - - - - - - - - - - - - - -
-
- - - - -
- {% block content %} - {% endblock %} -
-
-
- - - - diff --git a/labapp/app/templates/loadbalancing.html b/labapp/app/templates/loadbalancing.html new file mode 100644 index 0000000..35c23a7 --- /dev/null +++ b/labapp/app/templates/loadbalancing.html @@ -0,0 +1,209 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + +

+ 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. +

+ +
+ +

Exercise 1: AWS Cloud App

+

+ 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. +

+ +
+
    +
  • + + The URL for the cloud app hosted in AWS is https://aws-cloud-app.mcn-lab.f5demos.com +
  • +
  • + + The cloud app is only reachable from the student-awsnet site. +
  • +
  • + + The cloud app is TLS only. +
  • +
  • + + The load balancer domain is {{ ns or 'eph-ns' }}.mcn-lab.f5demos.com. +
  • +
  • + + Use the mcn-lab-wildcard wildcard cert in the shared NS to enable TLS on the LB. +
  • +
  • + + Do not advertise your load balancer to the internet. + {% if site %} Your site name is {{ site }}{% endif %} +
  • +
+
+ +
+ +

Test Criteria

+ +

+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. +

+ + +
+

+ + + +

+
+ +
+
+ temp +
+ temp +
+
+ + +
+
+ temp +
+
+ + +
+
+ temp +
+
+ +
+
+ + + + +

Exercise 2: Azure Cloud App

+ +

+ 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. +

+ + +
+
    +
  • + + The URL for the cloud app hosted in Azure is https://azure-cloud-app.mcn-lab.f5demos.com +
  • +
  • + + The cloud app is only reachable from the student-azurenet site. +
  • +
  • + + The cloud app is TLS only. +
  • +
  • + + Keep the AWS Origin Pool in place. +
  • +
+
+ + +
+ + +

Test Criteria

+ +

+GET https://{{ ns }}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns }}.mcn-lab.f5demos.com
+      
+
+ +

+{
+    "env": "Azure",
+    ...
+}
+        
+
+ + + + +
+ +
+
+ + + + + + + + + +
+{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/manipulation.html b/labapp/app/templates/manipulation.html new file mode 100644 index 0000000..f92c827 --- /dev/null +++ b/labapp/app/templates/manipulation.html @@ -0,0 +1,227 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + +

+ Since web traffic has been traversing proxies, engineers have needed to alter HTTP content for increased observability (XFF), performance (Cache-Control), or other core functionality (JWT). + "Proxy Pass" 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 implements these features granularly on routes or more 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. +

+ +
+
    +
  • + + Keep your configuration from the previous exercise in place. +
  • +
  • + + Requests to https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/aws/raw/ need to arrive at the origin with a path of /raw. +
  • +
+
+ +
+ +

Test Criteria

+ +

+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. +

+ +
+

+ + +

+
+
+ temp +
+
+ temp +
+
+
+ +
+ +

Exercise 2: Request Header Shenanigans

+ +

+ 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. +

+ +
+ +
+
    +
  • + + Insert a request header named X-MCN-src-site to identify the UDF CE to the origin. Do not use a static value. +
  • +
  • + + Insert a request header named X-MCN-namespace to identify the ephemeral NS to the origin. Do not use a static value. +
  • +
+
+ +
+ +

Test Criteria

+ +

+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": "cluster-xxxxxxxx",
+    },
+    ...
+}
+            
+
+ + +
+ +
+
+ + +
+ +

Exercise 3: Response Header Shenanigans

+ +
+ +
+
    +
  • + + Insert a response header named X-MCN-dest-site to determine which cloud CE processed the request. +
  • +
+
+ +
+ +

Test Criteria

+ + + + + +Request 1 +

+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"
+}
+                
+
+ +
+ +
+
+ + + + + + + + +
+{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/overview.html b/labapp/app/templates/overview.html new file mode 100644 index 0000000..4e77839 --- /dev/null +++ b/labapp/app/templates/overview.html @@ -0,0 +1,228 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + +

+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. +

+
+ +

Architecture

+ +

+The lab environment contains three distributed sites meshed using the F5 Distributed Cloud Global Network. +

+ +
    +
  • +     + student-awsnet in Amazon Web Services +
  • +
  • +     + student-azurenet in Microsoft Azure +
  • +
  • +     + Lab CE in UDF +
  • +
+ +
+ +
+ Arch +
+ +

Lab Exercises

+ +

+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. +

+ +
    +
  • + + Load Balancer +
  • +
  • + + Origin Pool +
  • +
  • + + Route +
  • +
+ +
+ +

Test Criteria

+ +

+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}}. +

+ +
+ +

Other Tools

+ +

curl and jq are provided on the UDF Runner instance.

+ +

+  ubuntu@ubuntu:~$ curl -s https://foo.mcn-lab.f5demos.com/ | jq
+  {
+    "info": {
+      "foo": true
+    }
+  }
+  
+ + + +
+ +

Cloud App

+ +

+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. +

+ +
+
+
+ + + +
+
+ + + +
+
+
+ +
+ +

Issues

+ +

+Use the lab repository issue tracker to report bugs, typos, or lab enhancements. +

+ + + +
+{% endblock %} + + + + + + + + + diff --git a/labapp/app/templates/portability.html b/labapp/app/templates/portability.html new file mode 100644 index 0000000..af22568 --- /dev/null +++ b/labapp/app/templates/portability.html @@ -0,0 +1,155 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + +

+ 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. +

+ +
+ +

Exercise 1: Advertise Policy

+ +
+
    +
  • + + Configure the load balancer to be advertised from the virtual site shared/mcn-practical-udf-sites. +
  • +
+
+ +
+ +

Test Criteria

+ +

+GET https://{{ ns or 'eph-ns '}}.mcn-lab.f5demos.com/ HTTP/1.1
+Host: {{ ns or 'eph-ns '}}.mcn-lab.f5demos.com
+                    
+
+ +

+{
+    "info": {
+        "path": "/"
+    }
+    ...
+}
+                
+
+ +
+ +
+
+ + +
+

+ +

+
+
+ temp +
+ temp +
+
+
+ +
+ +

Exercise 2: Find a Friend

+ +

+ Do you have a friend working on the lab? + Have they updated their advertise policy to use the virtual site? + Find thier ephemeral namespace (or use the one provided in the form). +

+ +
+
    +
  • + + Test if your friend's load balancer is being advertised to the UDF site. +
  • +
+
+ +
+ +

Test Criteria

+ + + +

+GET https://wiggly-yellowtail.mcn-lab.f5demos.com/ HTTP/1.1
+Host: wiggly-yellowtail.mcn-lab.f5demos.com
+                        
+
+ +

+{
+    "info": {
+        "path": "/"
+    }
+    ...
+}
+                    
+
+ +
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + +
+{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/route.html b/labapp/app/templates/route.html new file mode 100644 index 0000000..07cc901 --- /dev/null +++ b/labapp/app/templates/route.html @@ -0,0 +1,181 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + +

+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. +

+ +
+ +

Exercise 1: Path Routing

+ +

+Build routing rules and configure your load balancer to route traffic between the two cloud apps based on the request url. +

+ +
+
    +
  • + + Reuse the origin pools from the previous exercise. +
  • +
  • + + Route requests to https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/aws to the AWS cloud app. +
  • +
  • + + Route requests to https://{{ ns or 'eph-ns' }}.mcn-lab.f5demos.com/azure to the Azure cloud app. +
  • +
+
+ + +
+ +

Test Criteria

+ + + +Request 1 +

+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",
+    ...
+}
+        
+
+ +
+ +
+ +
+
+ + + + +

Exercise 2: Header Routing

+ +

+Build rules to route traffic between the two cloud apps based on an arbitrary HTTP request header. +

+ +
+
    +
  • + + Route requests with an X-MCN-Lab: aws header to the AWS cloud app. +
  • +
  • + + Route requests with an X-MCN-Lab: azure header to the Azure cloud app. +
  • +
+
+ +
+ +

Test Criteria

+ + + +Request 1 +

+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",
+    ...
+}
+            
+
+ +
+ +
+
+ + + + + + + + + +
+{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/score.html b/labapp/app/templates/score.html index 387cd53..a14e9f8 100644 --- a/labapp/app/templates/score.html +++ b/labapp/app/templates/score.html @@ -5,8 +5,33 @@ {% block content %} +
-

Scoreboard

+ + +
+ Banner +
+ +
@@ -36,15 +61,25 @@

Scoreboard

+
+ +
+ + -
- +
+ +
+

Feedback

+

+ + Please tell us your thoughts about the lab using this form. +

+
+

Issues

+

+ Use the lab repository issue tracker to report bugs, typos, or lab enhancements. +

- {% endblock %} diff --git a/labapp/app/templates/setup.html b/labapp/app/templates/setup.html index b97651d..b339c97 100644 --- a/labapp/app/templates/setup.html +++ b/labapp/app/templates/setup.html @@ -3,10 +3,50 @@ {% block title %}{{ title }}{% endblock %} {% block content %} -
- {{ content|safe }} + -
+
+ +
+ Banner +
+ + + +

+ 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. +

+ + eph-ns + +

+ The ephemeral namespace will be used to derive a unique URL for the load balancer used in the lab exercises. +

+ +
+
+ + +
+ + +
+ + +
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -23,7 +63,7 @@ diff --git a/labapp/app/templates/welcome.html b/labapp/app/templates/welcome.html new file mode 100644 index 0000000..dc6d119 --- /dev/null +++ b/labapp/app/templates/welcome.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+ +
+ Banner +
+ + + + + +
+ +

Getting Started

+ +

+ When your UDF deployment launched, two automated processes started - Customer Edge ("CE") registration and account provisioning in the lab tenant. +

+ +
+ +

Customer Edge

+ +

+ 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. +

+
+ +

Account Provisioning

+ +

+ 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. +

+ +
+
+
+ + + +
+
+ + + +
+
+
+ +
+ +

While You Wait

+ +
+ Here's a few things you can do while waiting for the CE to be registered and provisioned: +
+
    +
  • +     + Read the lab overview. +
  • +
  • +     + Check for the tenant welcome email. +
  • +
  • +     + Configure lab setup after logging into the tenant. +
  • +
  • +     + Get a cup of coffee. +
  • + +
+
+ + + + + + +
+{% endblock %} \ No newline at end of file