diff --git a/cloudapp/app/app.py b/cloudapp/app/app.py index a5d7a7b..a824f16 100644 --- a/cloudapp/app/app.py +++ b/cloudapp/app/app.py @@ -13,6 +13,14 @@ def create_app(): def to_pretty_json(value): return json.dumps(value, sort_keys=True, indent=4) + @app.errorhandler(401) + @app.errorhandler(404) + @app.errorhandler(500) + def return_err(err): + return { + 'error': err.description + } + @app.route('/raw', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) def echo(): """ @@ -33,6 +41,12 @@ def echo(): response['request_data'] = data return jsonify(response) + @app.route('//raw', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) + def env_echo(env): + if env.lower() == app.config['site'].lower(): + return echo() + return jsonify({'error': 'Invalid environment'}) + @app.route('/', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) @app.route('/echo', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) def echo_html(): diff --git a/cloudapp/app/zappa_settings.json b/cloudapp/app/zappa_settings.json index 7c4cb4f..55ea337 100644 --- a/cloudapp/app/zappa_settings.json +++ b/cloudapp/app/zappa_settings.json @@ -11,10 +11,30 @@ "profile_name": "default", "project_name": "cloudapp", "runtime": "python3.11", - "s3_bucket": "mcn-prac-cloudapp", + "s3_bucket": "mcn-prac-cloudapp-dev", "aws_region": "us-east-1", "environment_variables": { "SITE": "AWS" } + }, + "prod": { + "app_function": "app.app", + "exclude": [ + "boto3", + "dateutil", + "botocore", + "s3transfer", + "concurrent" + ], + "profile_name": "default", + "project_name": "cloudapp", + "runtime": "python3.11", + "s3_bucket": "mcn-prac-cloudapp-prod", + "aws_region": "us-east-1", + "environment_variables": { + "SITE": "AWS" + }, + "certificate_arn": "arn:aws:acm:us-east-1:317124676658:certificate/6149d976-8908-46f9-aa4a-e518b1dc16aa", + "domain": "aws-mcn-practical.f5demos.com" } } \ No newline at end of file diff --git a/labapp/Dockerfile b/labapp/Dockerfile index ac1341b..99564a8 100644 --- a/labapp/Dockerfile +++ b/labapp/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.10.14-slim +#FROM python:3.10.14-slim +FROM python:3.11-alpine3.18 LABEL org.opencontainers.image.description MCN Practical Lab App ENV PYTHONUNBUFFERED=1 @@ -6,8 +7,5 @@ WORKDIR /app COPY app . RUN pip install --no-cache-dir -r requirements.txt -EXPOSE 5000 -ENV FLASK_APP app.py -ENV FLASK_RUN_HOST 0.0.0.0 - -CMD ["flask", "run"] +EXPOSE 8080 +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"] diff --git a/labapp/app/__init__.py b/labapp/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/labapp/app/__init__.py @@ -0,0 +1 @@ + diff --git a/labapp/app/app.py b/labapp/app/app.py index 2146682..c0ade27 100644 --- a/labapp/app/app.py +++ b/labapp/app/app.py @@ -10,23 +10,26 @@ from ce import get_ce_info, get_ce_state app = Flask(__name__) -app.config['udf'] = os.getenv('UDF', False) -info = None -if app.config['udf']: - info = get_ce_info() -app.config['ce_info'] = info +app.config['ce_info'] = None +app.config['UDF'] = None +if os.getenv('UDF', None): + app.config['ce_info'] = get_ce_info() + app.config['UDF'] = True app.config['base_url'] = "mcn-lab.f5demos.com" app.config['CACHE_TYPE'] = 'SimpleCache' cache = Cache(app) app.secret_key = "blahblahblah" +class LabException(Exception): + """lab exception""" + def render_md(file: str) -> str: """render markdown w/ common extentions""" - with open(file, "r") as file: - content = file.read() + with open(file, "r", encoding="utf-8") as md: + content = md.read() html = markdown.markdown( content, - extensions=['markdown.extensions.attr_list','markdown.extensions.codehilite','markdown.extensions.fenced_code'] + extensions=['markdown.extensions.attr_list','markdown.extensions.fenced_code'] ) return html @@ -37,8 +40,33 @@ def validate_eph_ns(input_name): def eph_ns() -> str: """check if ephemeral namespace is set""" - eph_ns = request.cookies.get('eph_ns', None) - return eph_ns + this_eph_ns = request.cookies.get('eph_ns', None) + return this_eph_ns + +def cloudapp_fetch(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.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 + return data + +def headers_cleaner(headers): + """ + Remove headers that contain specific substrings. + """ + unwanted_substrings = ['x-envoy', 'cloudfront', 'x-k8se'] + filtered_headers = { + key: value for key, value in headers.items() + if not any(substring in key.lower() for substring in unwanted_substrings) + } + return filtered_headers @app.errorhandler(404) @app.errorhandler(500) @@ -54,7 +82,11 @@ def return_err(err): def index(): """index page""" html = render_md("markdown/overview.md") - return render_template('overview.html', content=html) + return render_template('standard.html', + title="MCN Practical: Overview", + content=html, + udf=app.config['UDF'] + ) @app.route('/setup', methods=['GET', 'POST']) def setup(): @@ -62,30 +94,37 @@ def setup(): if request.method == 'POST': action = request.form['action'] if action == 'save': - eph_ns = request.form['eph_ns'].strip() - print(eph_ns) - if not validate_eph_ns(eph_ns): + this_eph_ns = request.form['eph_ns'].strip() + if not validate_eph_ns(this_eph_ns): flash("Invalid ephemeral NS.", "danger") return redirect(url_for('setup')) response = make_response(redirect('/setup')) - response.set_cookie('eph_ns', eph_ns, max_age=60*60*24) + response.set_cookie('eph_ns', this_eph_ns, max_age=60*60*24) flash("Ephemeral NS successfully set.", "success") return response - elif action == 'clear': + if action == 'clear': response = make_response(redirect('/setup')) response.set_cookie('eph_ns', '', expires=0) flash("Ephemeral NS cleared.", "info") return response html = render_md("markdown/setup.md") - return render_template('setup.html', content=html) + 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', content=html, title="MCN Practical: Architecture") + return render_template('standard.html', + title="MCN Practical: Architecture", + content=html, + udf=app.config['UDF'] + ) -@app.route('/_ce_state') +@app.route('/_ce_status') @cache.cached(timeout=30) def ce_state(): """get ce state (internal route)""" @@ -97,50 +136,103 @@ def lb(): """lb page""" ns = eph_ns() html = render_md("markdown/lb.md") - return render_template('exercise_standard.html', title="MCN Practical: LB", content=html, ns=ns) + return render_template('exercise_standard.html', + title="MCN Practical: LB", + content=html, + ns=ns, + udf=app.config['UDF'] + ) -@app.route('/path') +@app.route('/route') def path(): - """path page""" + """routing page""" ns = eph_ns() - html = render_md("markdown/path.md") - return render_template('exercise_standard.html', title="MCN Practical: Path Routing", content=html, ns=ns) + html = render_md("markdown/route.md") + return render_template('exercise_standard.html', + title="MCN Practical: HTTP Routing", + content=html, + ns=ns, + udf=app.config['UDF'] + ) @app.route('/header') def header(): """header page""" ns = eph_ns() html = render_md("markdown/header.md") - return render_template('exercise_standard.html', title="MCN Practical: Headers", content=html, ns=ns) + return render_template('exercise_standard.html', + title="MCN Practical: Headers", + content=html, + ns=ns, + udf=app.config['UDF'] + ) -@app.route('/_lb_aws') +@app.route('/_lb1') def lb_aws(): - """AWS LB test""" + """Azure LB test""" try: ns = eph_ns() if not ns: - raise Exception("Ephemeral NS not set.") + raise LabException("Ephemeral NS not set") url = f"https://{ns}.{app.config['base_url']}/raw" - print(url) - response = requests.get(url, timeout=5) - print(response.text) - print(response.json()) - response.raise_for_status() - if response.json()['request_env'] != "AWS": - raise Exception("Invalid request env.") - return jsonify(status='success', data=response.json()) - except Exception as e: + data = cloudapp_fetch(url, 5, 'request_env', 'AWS') + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: return jsonify(status='fail', error=str(e)) -@app.route('/_lb_azure') +@app.route('/_lb2') def lb_azure(): """Azure LB test""" try: - response = requests.get('https://ifconfig1.io/all.json') - response.raise_for_status() - return jsonify(status='success', data=response.json()) - except requests.RequestException as e: + ns = eph_ns() + if not ns: + raise LabException("Ephemeral NS not set") + url = f"https://{ns}.{app.config['base_url']}/raw" + data = cloudapp_fetch(url, 5, 'request_env', 'Azure') + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: + return jsonify(status='fail', error=str(e)) + +@app.route('/_route1') +def route1(): + """First Route Test""" + try: + 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, 'request_env', 'AWS') + azure_data = cloudapp_fetch(azure_url, 5, 'request_env', 'Azure') + data = { + "aws": aws_data, + "azure": azure_data + } + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: + return jsonify(status='fail', error=str(e)) + +@app.route('/_route2') +def route2(): + """First Route Test""" + try: + 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, 'request_env', 'AWS', headers={"X-MCN-lab": "aws"}) + azure_data = cloudapp_fetch(azure_url, 5, 'request_env', 'Azure', headers={"X-MCN-lab": "azure"}) + data = { + "aws": aws_data, + "azure": azure_data + } + return jsonify(status='success', data=data) + except (LabException, requests.RequestException, ValueError) as e: return jsonify(status='fail', error=str(e)) + if __name__ == '__main__': - app.run(debug=False) \ No newline at end of file + app.run(host='0.0.0.0', port=5001, debug=False) \ No newline at end of file diff --git a/labapp/app/ce.py b/labapp/app/ce.py index 62d5c8f..70c6c61 100644 --- a/labapp/app/ce.py +++ b/labapp/app/ce.py @@ -135,7 +135,8 @@ def get_ce_state(ce_info: dict) -> dict: ce_state = response.json()['state'] return { "err": False, - "state": ce_state + "state": ce_state, + "site_name": ce_info['site_name'] } else: raise Exception(e) diff --git a/labapp/app/markdown/header.md b/labapp/app/markdown/header.md index e795016..01be8b0 100644 --- a/labapp/app/markdown/header.md +++ b/labapp/app/markdown/header.md @@ -1,5 +1,5 @@ # **Header Manipulation** @@ -20,7 +20,7 @@ HERE document.getElementById('requestBtn2').addEventListener('click', async () => { const resultDiv = document.getElementById('result2'); try { - const response = await axios.get('/_lb_azure'); + const response = await axios.get('/_head1'); if(response.data.status === 'success') { const prettyJson = JSON.stringify(response.data.data, null, 4); resultDiv.innerHTML = `
${prettyJson}
`; diff --git a/labapp/app/markdown/lb.md b/labapp/app/markdown/lb.md index b1d34f3..0f48503 100644 --- a/labapp/app/markdown/lb.md +++ b/labapp/app/markdown/lb.md @@ -16,63 +16,48 @@ More complicated configurations (underlay networking, security services, observa ### **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 following criteria: +Build an origin pool and load balancer based on the exercise requirements. + + +
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+#### **Test Criteria** +```http +GET https://eph-ns.mcn-lab.f5demos.com/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com -
+{ + "env": "aws", + ... +} +```
@@ -80,91 +65,86 @@ document.getElementById('requestBtn1').addEventListener('click', async () => { Since this is the first exercise, here are some hints (if you need them). +

-
-
-
- temp - temp -
+ +
+
+ temp + temp
-
-
-
- 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://aws-cloud-app.mcn-lab.f5demos.com +
  • +
  • +     + The cloud app is only reachable from the student-azurenet site. +
  • +
  • +     + The cloud app is TLS only. +
  • +
+#### **Test Criteria** + +```http +GET https://eph-ns.mcn-lab.f5demos.com/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com + +{ + "env": "azure", + ... +} +```
-
- -Once you've completed both exercises, move on to the path based routing exercise. + -
+Once you've completed both exercises, move on to the http routing exercise. diff --git a/labapp/app/markdown/path.md b/labapp/app/markdown/path.md deleted file mode 100644 index 00fc69f..0000000 --- a/labapp/app/markdown/path.md +++ /dev/null @@ -1,42 +0,0 @@ - - -# **Path Based Routing** - - - -
- -### **Exercise 1: Origin Routing** - -HERE - -
- -
-
- - -
- -Once you've completed both exercises, move on to the header manipulation exercise. - -
\ No newline at end of file diff --git a/labapp/app/markdown/route.md b/labapp/app/markdown/route.md new file mode 100644 index 0000000..763d354 --- /dev/null +++ b/labapp/app/markdown/route.md @@ -0,0 +1,126 @@ + + +# **HTTP Routing** + + + +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 provide 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 HTTP the request url. + +
    +
  • +     + Reuse the origin pools from the previous exercise +
  • +
  • +     + Route requests to https://ephemeral-ns.mcn-lab.f5demos.com/aws to the AWS cloud app. +
  • +
  • +     + Route requests to https://ephemeral-ns.mcn-lab.f5demos.com/azure to the Azure cloud app. +
  • +
+ +
+ +#### **Test Criteria** + +```http +GET https://eph-ns.mcn-lab.f5demos.com/aws/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com + +{ + "env": "aws", + ... +} +``` + +```http +GET https://eph-ns.mcn-lab.f5demos.com/azure/raw HTTP/1.1 +Host: 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** + +```http +GET https://eph-ns.mcn-lab.f5demos.com/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com +X-MCN-lab: aws + +{ + "env": "aws", + ... +} +``` + +```http +GET https://eph-ns.mcn-lab.f5demos.com/raw HTTP/1.1 +Host: eph-ns.mcn-lab.f5demos.com +X-MCN-lab: azure + +{ + "env": "azure", + ... +} +``` + +
+ +
+
+ + + + +Once you've completed the exercises, move on to the header manipulation exercise. \ No newline at end of file diff --git a/labapp/app/requirements.txt b/labapp/app/requirements.txt index a7620cc..7c54f96 100644 --- a/labapp/app/requirements.txt +++ b/labapp/app/requirements.txt @@ -4,4 +4,5 @@ Markdown ~=3.6 validators ~=0.28.1 boto3 ~=1.34.92 pyYAML ~=6.0.1 -Flask-Caching ~=2.1.0 \ No newline at end of file +Flask-Caching ~=2.1.0 +gunicorn ~=22.0.0 \ No newline at end of file diff --git a/labapp/app/static/bad.png b/labapp/app/static/bad.png new file mode 100644 index 0000000..296be54 Binary files /dev/null and b/labapp/app/static/bad.png differ diff --git a/labapp/app/static/custom.css b/labapp/app/static/custom.css index 96a865b..002e5a3 100644 --- a/labapp/app/static/custom.css +++ b/labapp/app/static/custom.css @@ -1,6 +1,5 @@ body { - min-height: 100vh; min-height: -webkit-fill-available; } @@ -19,6 +18,19 @@ main { overflow-y: auto; } +.row { + height: 100%; +} + +.status-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: auto; +} + + .markdown-body { padding: 50px; } diff --git a/labapp/app/static/dracula.css b/labapp/app/static/dracula.css new file mode 100644 index 0000000..087149f --- /dev/null +++ b/labapp/app/static/dracula.css @@ -0,0 +1,81 @@ +/* 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/good.png b/labapp/app/static/good.png new file mode 100644 index 0000000..9436013 Binary files /dev/null and b/labapp/app/static/good.png differ diff --git a/labapp/app/static/header-manipulation.png b/labapp/app/static/header.png similarity index 100% rename from labapp/app/static/header-manipulation.png rename to labapp/app/static/header.png diff --git a/labapp/app/static/lb-icon.png b/labapp/app/static/lb-icon.png new file mode 100644 index 0000000..7a5dd88 Binary files /dev/null and b/labapp/app/static/lb-icon.png differ diff --git a/labapp/app/static/origin-icon.png b/labapp/app/static/origin-icon.png new file mode 100644 index 0000000..51682f1 Binary files /dev/null and b/labapp/app/static/origin-icon.png differ diff --git a/labapp/app/static/tls-icon.png b/labapp/app/static/tls-icon.png new file mode 100644 index 0000000..51e2b30 Binary files /dev/null and b/labapp/app/static/tls-icon.png differ diff --git a/labapp/app/templates/base.html b/labapp/app/templates/base.html index c601e0b..045fa49 100644 --- a/labapp/app/templates/base.html +++ b/labapp/app/templates/base.html @@ -2,17 +2,18 @@ - {% block title %}F5XC MCN Practical Lab{% endblock %} + {% block title %}{{ title }}{% endblock %} - - - - - + + + + + + @@ -55,7 +56,6 @@ } document.cookie = name + "=" + (value || "") + expires + "; path=/"; } - function getCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); @@ -66,6 +66,26 @@ } return null; } + async function makeHttpRequest(buttonId, requestUrl, resultDivId) { + const button = document.getElementById(buttonId); + const resultDiv = document.getElementById(resultDivId); + 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 = `
Success:
${prettyJson}
`; + } else { + const errJson = JSON.stringify(response.data.error, null, 4); + resultDiv.innerHTML = `
Request Failed:  ${errJson}
`; + } + } catch (error) { + resultDiv.innerHTML = `
Error: ${error.message}
`; + } finally { + button.disabled = false; + resultDiv.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + } @@ -74,7 +94,6 @@
+ {% if udf %} + +
+

Loading...

+ Status +
+ {% endif %} +
+
diff --git a/labapp/app/templates/index.html.delete b/labapp/app/templates/index.html.delete deleted file mode 100644 index 53b2aaa..0000000 --- a/labapp/app/templates/index.html.delete +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} - -{% block title %}MCN Practical: Overview{% endblock %} - -{% block content %} -

HTTP Request Maker

- -
- - -{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/overview.html b/labapp/app/templates/overview.html deleted file mode 100644 index 9b87381..0000000 --- a/labapp/app/templates/overview.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "base.html" %} - -{% block title %}MCN Practical Overview{% endblock %} - -{% block content %} -
- {{ content|safe }} -
-{% endblock %} \ No newline at end of file diff --git a/labapp/app/templates/setup.html b/labapp/app/templates/setup.html index 6d79535..cbf7e19 100644 --- a/labapp/app/templates/setup.html +++ b/labapp/app/templates/setup.html @@ -1,11 +1,12 @@ {% extends "base.html" %} -{% block title %}MCN Practical: Setup{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block content %}
{{ content|safe }} +
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -15,6 +16,7 @@
{% endif %} {% endwith %} +
{% endblock %} diff --git a/labapp/app/templates/setup.html.delete b/labapp/app/templates/setup.html.delete deleted file mode 100644 index 70a3a33..0000000 --- a/labapp/app/templates/setup.html.delete +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Base Domain Setup

-
-
- - -
- - -
- - - -{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} -{% endwith %} -{% endblock %} - diff --git a/labapp/labapp_installer.sh b/labapp/labapp_installer.sh new file mode 100644 index 0000000..a162229 --- /dev/null +++ b/labapp/labapp_installer.sh @@ -0,0 +1,73 @@ +#!/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 +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 + +# Create directories +mkdir -p $SCRIPTDIR +mkdir -p $APPDIR + +# Create the start_labapp.sh script +cat <$SCRIPTDIR/start_app.sh +#!/bin/bash + +if [ ! -d "$APPDIR/.git" ]; then + git clone -b $BRANCH $REPO_URL $APPDIR +else + cd $APPDIR + # Ensure that the local repository is tracking the correct remote and branch + git remote set-url origin $REPO_URL + git fetch --all + # Reset to the specified branch forcefully + git checkout $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 + +# Start the Gunicorn server +gunicorn --workers 4 --bind 0.0.0.0:1337 app:app +EOF + +# Make the script executable +chmod +x $SCRIPTDIR/start_app.sh + +# Create systemd service file +cat </etc/systemd/system/$SERVICE +[Unit] +Description=MCN Practical Lab App +After=network.target + +[Service] +WorkingDirectory=$APPDIR +ExecStart=/bin/bash $SCRIPTDIR/start_app.sh +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +# Reload systemd, enable and start the service +systemctl daemon-reload +systemctl enable $SERVICE +systemctl start $SERVICE + +echo "$SERVICE has been installed and started as a systemd service."