From: demskeq8 Date: Wed, 20 Mar 2024 07:50:14 +0000 (+0100) Subject: [Solution] Update to ONAP Montreal releases version X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=ef65699bf395a9c7c678f613968e87be713ebf8a;p=oam.git [Solution] Update to ONAP Montreal releases version - update controller/odlux images - change persistence from elasticsearch to mariaDB - improved user handling for identity - support indentity theme Issue-ID: OAM-403 Change-Id: I28bec223a656af1dcfe61b47f5df43fba4545a52 Signed-off-by: demskeq8 --- diff --git a/solution/README.md b/solution/README.md index 036b803..7aa454f 100644 --- a/solution/README.md +++ b/solution/README.md @@ -111,7 +111,7 @@ For development purposes and may reference the ``` $ cat /etc/hosts 127.0.0.1 localhost -127.0.1.1 +127.0.1.1 10.20.35.165 # SMO OAM development system smo.o-ran-sc.org @@ -136,8 +136,14 @@ $ cat /etc/hosts The following commands should be invoked. More detailed can be found in the next chapters. -``` -docker compose -f smo/common/docker-compose.yaml up -d +```bash +docker compose -f smo/common/docker-compose.yaml up -d --wait + +# optionally adjust the users.csv file to create new users +vim users.csv +# override authentication.json with the new users +python3 create_users.py users.csv -o smo/common/identity/authentication.json + python smo/common/identity/config.py docker compose -f smo/oam/docker-compose.yaml up -d diff --git a/solution/adopt_to_environment.py b/solution/adopt_to_environment.py index 0993659..9b531cc 100755 --- a/solution/adopt_to_environment.py +++ b/solution/adopt_to_environment.py @@ -17,6 +17,7 @@ import os import argparse +from jinja2 import Template default_ip_address = 'aaa.bbb.ccc.ddd' default_http_domain = 'smo.o-ran-sc.org' @@ -27,9 +28,13 @@ file_extensions = ['.env', '.yaml', '.json'] parser = argparse.ArgumentParser(script_name) required = parser.add_argument_group('required named arguments') -required.add_argument("-i", "--ip_address", help="The remote accessable IP address of this system.", type=str, required=True) -parser.add_argument("-d", "--http_domain", help="The http domain. Default is " + default_http_domain + ".", type=str, default=default_http_domain) -parser.add_argument("-r", "--revert", help="Reverts the previous made changes.", action='store_true') +required.add_argument("-i", "--ip_address", help="The remote accessable IP address of this system.", + type=str, required=True) +parser.add_argument("-d", "--http_domain", help="The http domain. Default is " + + default_http_domain + ".", + type=str, default=default_http_domain) +parser.add_argument("-r", "--revert", help="Reverts the previous made changes.", + action='store_true') args = parser.parse_args() def find_replace(directory, find_text, replace_text, extensions): @@ -45,6 +50,33 @@ def find_replace(directory, find_text, replace_text, extensions): with open(file_path, 'w') as file: file.write(updated_content) print(f"Replaced '{find_text}' with '{replace_text}' in '{file_path}'") +def create_etc_hosts(ip_adress_v4: str, http_domain: str ) -> None: + """ + creates scelaton for /etc/hosts and writes to local file + @param ip_adress: ipv4 address of the system + @param http_domain: base domain name for the deployment + """ + template_str = """ +# SMO OAM development system +{{ deployment_system_ipv4 }} {{ http_domain }} +{{ deployment_system_ipv4 }} gateway.{{ http_domain }} +{{ deployment_system_ipv4 }} identity.{{ http_domain }} +{{ deployment_system_ipv4 }} messages.{{ http_domain }} +{{ deployment_system_ipv4 }} kafka-bridge.{{ http_domain }} +{{ deployment_system_ipv4 }} odlux.oam.{{ http_domain }} +{{ deployment_system_ipv4 }} flows.oam.{{ http_domain }} +{{ deployment_system_ipv4 }} tests.oam.{{ http_domain }} +{{ deployment_system_ipv4 }} controller.dcn.{{ http_domain }} +{{ deployment_system_ipv4 }} ves-collector.dcn.{{ http_domain }} + +""" + template = Template(template_str) + hosts_entries: str = template.render(deployment_system_ipv4=ip_adress_v4, + http_domain=http_domain) + output_txt_path = f"{directory_path}/append_to_etc_hosts.txt" + with open(output_txt_path, 'w', encoding="utf-8") as f: + f.write(hosts_entries) + print(f"/etc/hosts entries created: {output_txt_path}") if args.revert == False: # replace ip @@ -52,11 +84,14 @@ if args.revert == False: # replace domain if not args.http_domain == default_http_domain: - find_replace(directory_path, default_http_domain, args.http_domain, file_extensions) + find_replace(directory_path, default_http_domain, args.http_domain, file_extensions) + # write append file for etc/hosts + create_etc_hosts(ip_adress_v4=args.ip_address, http_domain=args.http_domain) else: # revert back ip find_replace(directory_path, args.ip_address, default_ip_address, file_extensions) # revert back domain if not args.http_domain == default_http_domain: - find_replace(directory_path, args.http_domain, default_http_domain, file_extensions) + find_replace(directory_path, args.http_domain, default_http_domain, file_extensions) + diff --git a/solution/append_to_etc_hosts.sh b/solution/append_to_etc_hosts.sh new file mode 100755 index 0000000..779dc48 --- /dev/null +++ b/solution/append_to_etc_hosts.sh @@ -0,0 +1,4 @@ +#!/bin/bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cat $SCRIPT_DIR/append_to_etc_hosts.txt >> /etc/hosts +cat /etc/hosts \ No newline at end of file diff --git a/solution/create_users.py b/solution/create_users.py new file mode 100644 index 0000000..5b0964c --- /dev/null +++ b/solution/create_users.py @@ -0,0 +1,88 @@ +import csv +import json +from argparse import ArgumentParser + +from jinja2 import Template + + +class UserCreator: + template_str = """ + { + "users": [ + {% for user in users %} + { + "firstName": "{{ user.firstName }}", + "lastName": "{{ user.lastName }}", + "email": "{{ user.email }}", + "enabled": "{{ user.enabled }}", + "username": "{{ user.username }}", + "credentials": [ + { + "type": "password", + "value": "{{ user.password }}", + {% if force_pwd_change is false %} + "temporary": "{{ user.force_pwd_change }}" + {% else %} + "temporary": "true" + {% endif %} + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "grants": [ + {% for user in users %} + { + "username": "{{ user.username }}", + "role": "{{ user.role }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + """ + + def __init__(self, csv_file: str): + self.csv_file_path: str = csv_file + + def get_users_from_csv(self) -> list[dict]: + """ + Get the users from the CSV file + @return: list of users + """ + users: list[dict] = [] + with open(self.csv_file_path, "r") as file: + dict_reader: csv.DictReader = csv.DictReader(file) + for row in dict_reader: + users.append(row) + return users + + def create_users_json(self, users: list[dict], output_json_path: str, force_pwd_change: bool ) -> None: + """ + Create the users JSON from the users list. Uses Jinja2 template to create the JSON. + @param users: list of users to create the JSON + @param output_json_path: path to the output JSON file + @return: JSON string + """ + if force_pwd_change is True: + print("Enforce password change for all users!") + template = Template(self.template_str) + users_json: str = template.render(users=users, force_pwd_change=force_pwd_change) + with open(output_json_path, 'w') as f: + json.dump(json.loads(users_json), f, indent=4) + print(f"Users JSON file created at {output_json_path}") + + +if __name__ == '__main__': + parser = ArgumentParser(description="Create users JSON file from CSV file") + parser.add_argument("csv_file_path", type=str, help="Path to the CSV file containing the users data") + parser.add_argument("--output", "-o", type=str, required=False, default="authentication.json", + help="Path to the output JSON file e.g. authentication.json") + parser.add_argument("--force-pwd-change","-f", action='store_true', help="Enforce password change for all users, overwrites value in user data" ) + + args = parser.parse_args() + user_creator = UserCreator(args.csv_file_path) + user_list = user_creator.get_users_from_csv() + user_creator.create_users_json(user_list, args.output, args.force_pwd_change) diff --git a/solution/setup.sh b/solution/setup.sh new file mode 100755 index 0000000..e434fa0 --- /dev/null +++ b/solution/setup.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +docker compose -f $SCRIPT_DIR/smo/common/docker-compose.yaml up -d --wait +python3 create_users.py $SCRIPT_DIR/users.csv -o $SCRIPT_DIR/smo/common/identity/authentication.json +python3 $SCRIPT_DIR/smo/common/identity/config.py +docker compose -f $SCRIPT_DIR/smo/oam/docker-compose.yaml up -d + + + diff --git a/solution/smo/common/.env b/solution/smo/common/.env index cc337b4..392b892 100644 --- a/solution/smo/common/.env +++ b/solution/smo/common/.env @@ -38,7 +38,7 @@ IDENTITY_MGMT_PASSWORD=Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U IDENTITY_PROVIDER_URL=https://identity.${HTTP_DOMAIN} # PERSISTENCE (including SDN-R Database) -PERSISTENCE_IMAGE=docker.elastic.co/elasticsearch/elasticsearch-oss:7.9.3 +PERSISTENCE_IMAGE=mariadb:11.1.2 ## ZooKeeper ZOOKEEPER_IMAGE=nexus3.onap.org:10001/onap/dmaap/zookeeper:6.0.3 diff --git a/solution/smo/common/docker-compose.yaml b/solution/smo/common/docker-compose.yaml index 7afa49b..772d073 100755 --- a/solution/smo/common/docker-compose.yaml +++ b/solution/smo/common/docker-compose.yaml @@ -14,27 +14,29 @@ # limitations under the License. # # no more versions needed! Compose spec supports all features w/o a version +version: "3.8" services: - gateway: image: ${TRAEFIK_IMAGE} container_name: gateway hostname: gateway healthcheck: test: - - CMD - - traefik - - healthcheck - - --ping + [ + "CMD", + "traefik", + "healthcheck", + "--ping" + ] interval: 10s timeout: 5s retries: 3 restart: always ports: - - 80:80 - - 443:443 - - 4334:4334 - - 4335:4335 + - "80:80" + - "443:443" + - "4334:4334" + - "4335:4335" command: - --serverstransport.insecureskipverify=true - --log.level=${TRAEFIK_LOG_LEVEL} @@ -72,6 +74,9 @@ services: traefik.http.middlewares.strip.stripprefix.prefixes: /traefik traefik.http.routers.gateway.tls: true traefik.http.services.gateway.loadbalancer.server.port: 8080 + app: "gateway" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" networks: dmz: dcn: @@ -81,44 +86,53 @@ services: container_name: identitydb hostname: identitydb environment: - - ALLOW_EMPTY_PASSWORD=no - - POSTGRESQL_USERNAME=keycloak - - POSTGRESQL_DATABASE=keycloak - - POSTGRESQL_PASSWORD=keycloak + ALLOW_EMPTY_PASSWORD: no + POSTGRESQL_USERNAME: keycloak + POSTGRESQL_DATABASE: keycloak + POSTGRESQL_PASSWORD: keycloak + labels: + app: "identitydb" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" identity: image: ${IDENTITY_IMAGE} container_name: identity hostname: identity environment: - - KEYCLOAK_CREATE_ADMIN_USER=true - - KEYCLOAK_ADMIN_USER=${ADMIN_USERNAME} - - KEYCLOAK_ADMIN_PASSWORD=${ADMIN_PASSWORD} - - KEYCLOAK_MANAGEMENT_USER=${IDENTITY_MGMT_USERNAME} - - KEYCLOAK_MANAGEMENT_PASSWORD=${IDENTITY_MGMT_PASSWORD} - - KEYCLOAK_DATABASE_HOST=identitydb - - KEYCLOAK_DATABASE_NAME=keycloak - - KEYCLOAK_DATABASE_USER=keycloak - - KEYCLOAK_DATABASE_PASSWORD=keycloak - - KEYCLOAK_JDBC_PARAMS=sslmode=disable&connectTimeout=30000 - - KEYCLOAK_PRODUCTION=false - - KEYCLOAK_ENABLE_TLS=true - - KEYCLOAK_TLS_KEYSTORE_FILE=/opt/bitnami/keycloak/certs/keystore.jks - - KEYCLOAK_TLS_TRUSTSTORE_FILE=/opt/bitnami/keycloak/certs/truststore.jks - - KEYCLOAK_TLS_KEYSTORE_PASSWORD=password - - KEYCLOAK_TLS_TRUSTSTORE_PASSWORD=changeit + KEYCLOAK_CREATE_ADMIN_USER: true + KEYCLOAK_ADMIN_USER: ${ADMIN_USERNAME} + KEYCLOAK_ADMIN_PASSWORD: ${ADMIN_PASSWORD} + KEYCLOAK_MANAGEMENT_USER: ${IDENTITY_MGMT_USERNAME} + KEYCLOAK_MANAGEMENT_PASSWORD: ${IDENTITY_MGMT_PASSWORD} + KEYCLOAK_DATABASE_HOST: identitydb + KEYCLOAK_DATABASE_NAME: keycloak + KEYCLOAK_DATABASE_USER: keycloak + KEYCLOAK_DATABASE_PASSWORD: keycloak + KEYCLOAK_JDBC_PARAMS=sslmode: disable&connectTimeout=30000 + KEYCLOAK_PRODUCTION: false + KEYCLOAK_ENABLE_TLS: true + KEYCLOAK_TLS_KEYSTORE_FILE: /opt/bitnami/keycloak/certs/keystore.jks + KEYCLOAK_TLS_TRUSTSTORE_FILE: /opt/bitnami/keycloak/certs/truststore.jks + KEYCLOAK_TLS_KEYSTORE_PASSWORD: password + KEYCLOAK_TLS_TRUSTSTORE_PASSWORD: changeit + KEYCLOAK_EXTRA_ARGS: "--spi-theme-default=oam" restart: unless-stopped volumes: - /etc/localtime:/etc/localtime:ro - ./identity/standalone.xml:/opt/jboss/keycloak/standalone/configuration/standalone.xml - ./identity/keystore.jks:/opt/bitnami/keycloak/certs/keystore.jks - ./identity/truststoreONAPall.jks:/opt/bitnami/keycloak/certs/truststore.jks + - ./identity/themes/oam:/opt/bitnami/keycloak/themes/oam labels: traefik.enable: true traefik.http.routers.identity.entrypoints: websecure traefik.http.routers.identity.rule: Host(`identity.${HTTP_DOMAIN}`) traefik.http.routers.identity.tls: true traefik.http.services.identity.loadbalancer.server.port: 8080 + app: "identity" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" depends_on: identitydb: condition: service_started @@ -132,7 +146,31 @@ services: image: ${PERSISTENCE_IMAGE} container_name: persistence environment: - - discovery.type=single-node + MARIADB_ROOT_PASSWORD: admin + MARIADB_DATABASE: sdnrdb + MARIADB_USER: sdnrdb + MARIADB_PASSWORD: sdnrdb + MARIADB_EXTRA_FLAGS: --bind-address=* --max_connections=400 + MYSQL_ROOT_PASSWORD: admin + MYSQL_DATABASE: sdnrdb + MYSQL_USER: sdnrdb + MYSQL_PASSWORD: sdnrdb + labels: + app: "persistence" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" + healthcheck: + interval: 30s + retries: 3 + test: + [ + "CMD", + "healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized" + ] + timeout: 30s zookeeper: image: ${ZOOKEEPER_IMAGE} @@ -150,6 +188,10 @@ services: ZOOKEEPER_SERVER_ID: volumes: - ./zookeeper/zk_server_jaas.conf:/etc/zookeeper/secrets/jaas/zk_server_jaas.conf + labels: + app: "zookeeper" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" kafka: image: ${KAFKA_IMAGE} @@ -169,6 +211,10 @@ services: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 # Reduced the number of partitions only to avoid the timeout error for the first subscribe call in slow environment KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS: 1 + labels: + app: "kafka" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" volumes: - ./kafka/zk_client_jaas.conf:/etc/kafka/secrets/jaas/zk_client_jaas.conf depends_on: @@ -192,6 +238,9 @@ services: traefik.http.routers.kafka-bridge.rule: Host(`kafka-bridge.${HTTP_DOMAIN}`) traefik.http.routers.kafka-bridge.tls: true traefik.http.services.kafka-bridge.loadbalancer.server.port: 8080 + app: "kafka-bridge" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" volumes: - ./kafka-bridge:/opt/strimzi/config depends_on: @@ -222,6 +271,9 @@ services: traefik.http.routers.topology.rule: Host(`topology.${HTTP_DOMAIN}`) traefik.http.routers.topology.tls: true traefik.http.services.topology.loadbalancer.server.port: 8181 + app: "topology" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" networks: dmz: default: @@ -242,6 +294,9 @@ services: traefik.http.routers.messages.rule: Host(`messages.${HTTP_DOMAIN}`) traefik.http.routers.messages.tls: true traefik.http.services.messages.loadbalancer.server.port: 3904 + app: "messages" + deploy: "o-ran-sc-smo-common" + solution: "o-ran-sc-smo" depends_on: kafka: condition: service_started diff --git a/solution/smo/common/identity/authentication.json b/solution/smo/common/identity/authentication.json index 2f91979..868c790 100644 --- a/solution/smo/common/identity/authentication.json +++ b/solution/smo/common/identity/authentication.json @@ -1,111 +1,111 @@ { - "users": [ - { - "firstName": "Leia", - "lastName": "Organa", - "email": "leia.organa@sdnr.onap.org", - "enabled": "true", - "username": "leia.organa", - "credentials": [ + "users": [ { - "type": "password", - "value": "Default4SDN!", - "temporary": true - } - ], - "requiredActions": [ - "UPDATE_PASSWORD" - ] - }, - { - "firstName": "R2", - "lastName": "D2", - "email": "r2.d2@sdnr.onap.org", - "enabled": "true", - "username": "r2.d2", - "credentials": [ + "firstName": "Leia", + "lastName": "Organa", + "email": "leia.organa@sdnr.onap.org", + "enabled": "", + "username": "leia.organa", + "credentials": [ + { + "type": "password", + "value": "Default4SDN!", + "temporary": "true" + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] + }, { - "type": "password", - "value": "Default4SDN!", - "temporary": true - } - ], - "requiredActions": [ - "UPDATE_PASSWORD" - ] - }, - { - "firstName": "Luke", - "lastName": "Skywalker", - "email": "luke.skywalker@sdnr.onap.org", - "enabled": "true", - "username": "luke.skywalker", - "credentials": [ + "firstName": "R2", + "lastName": "D2", + "email": "r2.d2@sdnr.onap.org", + "enabled": "", + "username": "r2.d2", + "credentials": [ + { + "type": "password", + "value": "Default4SDN!", + "temporary": "true" + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] + }, { - "type": "password", - "value": "Default4SDN!", - "temporary": true - } - ], - "requiredActions": [ - "UPDATE_PASSWORD" - ] - }, - { - "firstName": "Jargo", - "lastName": "Fett", - "email": "jargo.fett@sdnr.onap.org", - "enabled": "true", - "username": "jargo.fett", - "credentials": [ + "firstName": "Luke", + "lastName": "Skywalker", + "email": "luke.skywalker@sdnr.onap.org", + "enabled": "", + "username": "luke.skywalker", + "credentials": [ + { + "type": "password", + "value": "Default4SDN!", + "temporary": "true" + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] + }, + { + "firstName": "Jargo", + "lastName": "Fett", + "email": "jargo.fett@sdnr.onap.org", + "enabled": "", + "username": "jargo.fett", + "credentials": [ + { + "type": "password", + "value": "Default4SDN!", + "temporary": "true" + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] + }, { - "type": "password", - "value": "Default4SDN!", - "temporary": true + "firstName": "Martin", + "lastName": "Skorupski", + "email": "martin.skorupski@highstreet-technologies.com", + "enabled": "", + "username": "martin.skorupski", + "credentials": [ + { + "type": "password", + "value": "Default4SDN!", + "temporary": "true" + } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ] } - ], - "requiredActions": [ - "UPDATE_PASSWORD" - ] - }, - { - "firstName": "Martin", - "lastName": "Skorupski", - "email": "martin.skorupski@highstreet-technologies.com", - "enabled": "true", - "username": "martin.skorupski", - "credentials": [ + ], + "grants": [ + { + "username": "leia.organa", + "role": "administration" + }, + { + "username": "r2.d2", + "role": "administration" + }, + { + "username": "luke.skywalker", + "role": "provision" + }, + { + "username": "jargo.fett", + "role": "supervision" + }, { - "type": "password", - "value": "Default4SDN!", - "temporary": true + "username": "martin.skorupski", + "role": "administration" } - ], - "requiredActions": [ - "UPDATE_PASSWORD" - ] - } - ], - "grants": [ - { - "username": "leia.organa", - "role": "administration" - }, - { - "username": "r2.d2", - "role": "administration" - }, - { - "username": "luke.skywalker", - "role": "provision" - }, - { - "username": "jargo.fett", - "role": "supervision" - }, - { - "username": "martin.skorupski", - "role": "administration" - } - ] + ] } \ No newline at end of file diff --git a/solution/smo/common/identity/config.py b/solution/smo/common/identity/config.py index 5a8bf44..37967e1 100644 --- a/solution/smo/common/identity/config.py +++ b/solution/smo/common/identity/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -############################################################################# -# Copyright 2023 highstreet technologies GmbH +################################################################################ +# Copyright 2021 highstreet technologies GmbH # # Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. @@ -16,26 +16,27 @@ # # importing the sys, json, requests library +from sqlite3 import TimeFromTicks +from jproperties import Properties import os -import pathlib import sys import json import time -import getpass -import requests import re +import requests +import getpass import warnings -from jproperties import Properties from typing import List + warnings.filterwarnings('ignore', message='Unverified HTTPS request') -# global configurations -def get_environment_variable(name): +# global configurations +def get_env(name): configs = Properties() - path = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) - env_file = str(path.parent.absolute()) + '/.env' - with open(env_file, "rb") as read_prop: + envFile = os.path.dirname(os.path.abspath(__file__)) + '/' + '../' + '.env' + + with open(envFile, "rb") as read_prop: configs.load(read_prop) value = configs.get(name).data @@ -45,54 +46,61 @@ def get_environment_variable(name): match = next(matches, None) if match is None: break - inner = get_environment_variable(match.group(1)) - value = value.replace("${" + match.group(1) + "}", inner ) + inner = get_env(match.group(1)) + value = value.replace("${" + match.group(1) + "}", inner) return value -def load_arguments(args: List[str]) -> tuple: - realm_file = os.path.dirname(os.path.abspath( - __file__)) + '/o-ran-sc-realm.json' - auth_file = os.path.dirname(os.path.abspath( - __file__)) + '/authentication.json' - ready_timeout = 180 +def loadArgs(args: List[str]) -> tuple: + realmFile = os.path.dirname(os.path.abspath(__file__)) + '/o-ran-sc-realm.json' + authFile = os.path.dirname(os.path.abspath(__file__)) + '/authentication.json' + readyTimeout = 180 args.pop(0) while len(args) > 0: arg = args.pop(0) if arg == '--auth' and len(args) > 0: - auth_file = args.pop(0) - print('overwriting auth file: {}'.format(auth_file)) + authFile = args.pop(0) + print('overwriting auth file: {}'.format(authFile)) elif arg == '--realm' and len(args) > 0: - realm_file = args.pop(0) - print('overwriting realm file: {}'.format(realm_file)) + realmFile = args.pop(0) + print('overwriting realm file: {}'.format(realmFile)) elif arg == '--timeout' and len(args) > 0: - ready_timeout = int(args.pop(0)) - print('waiting for ready {} seconds'.format(ready_timeout)) + readyTimeout = int(args.pop(0)) + print('waiting for ready {} seconds'.format(readyTimeout)) - return (realm_file, auth_file, ready_timeout) + return (realmFile, authFile, readyTimeout) def isReady(timeoutSeconds=180): url = getBaseUrl() - print(f'url={url}') + print(url) + response = None + print("waiting for ready state", end='') while timeoutSeconds > 0: try: response = requests.get(url, verify=False, headers={}) + print(response) except: - response = None + pass if response is not None and response.status_code == 200: + print('succeeded') return True time.sleep(1) timeoutSeconds -= 1 + print('.', end='', flush=True) return False def getBaseUrl(): - return get_environment_variable("IDENTITY_PROVIDER_URL") - -# Request a token for further communication + try: + if get_env("USE_LOCAL_HOST_FOR_IDENTITY_CONFIG").strip("'\"") == "true": + return get_env("IDENTITY_PROVIDER_URL_LOCAL_HOST") + except AttributeError: + print("Using IDENTITY_PROVIDER_URL") + return get_env("IDENTITY_PROVIDER_URL") +# Request a token for futher communication def getToken(): url = base + '/realms/master/protocol/openid-connect/token' headers = { @@ -106,8 +114,7 @@ def getToken(): 'password': password } try: - response = requests.post(url, verify=False, auth=( - username, password), data=body, headers=headers) + response = requests.post(url, verify=False, auth=(username, password), data=body, headers=headers) except requests.exceptions.Timeout: sys.exit('HTTP request failed, please check you internet connection.') except requests.exceptions.TooManyRedirects: @@ -122,9 +129,8 @@ def getToken(): else: sys.exit('Getting token failed.') -# create the default realm from file - +# create the default realm from file def createRealm(token, realm): url = base + '/admin/realms' auth = 'bearer ' + token @@ -134,8 +140,7 @@ def createRealm(token, realm): 'authorization': auth } try: - response = requests.post( - url, verify=False, json=realm, headers=headers) + response = requests.post(url, verify=False, json=realm, headers=headers) except requests.exceptions.Timeout: sys.exit('HTTP request failed, please check you internet connection.') except requests.exceptions.TooManyRedirects: @@ -146,9 +151,8 @@ def createRealm(token, realm): return response.status_code >= 200 and response.status_code < 300 -# Check if default realm exists - +# Check if default realm exists def checkRealmExists(token, realmId): url = base + '/admin/realms/' + realmId auth = 'bearer ' + token @@ -172,9 +176,8 @@ def checkRealmExists(token, realmId): # sys.exit('Getting realm failed.') return False -# create a user in default realm - +# create a user in default realm def createUser(token, realmConfig, user): realmId = realmConfig['id'] url = base + '/admin/realms/' + realmId + '/users' @@ -198,9 +201,8 @@ def createUser(token, realmConfig, user): else: print('User creation', user['username'], 'failed!\n', response.text) -# creates User accounts in realm based a file - +# creates User accounts in realm based a file def createUsers(token, realmConfig, authConfig): for user in authConfig['users']: createUser(token, realmConfig, user) @@ -216,24 +218,24 @@ def createUsers(token, realmConfig, authConfig): { "type": "password", "value": password, - "temporary": True + "temporary": False } - ], - "requiredActions": [ - "UPDATE_PASSWORD" ] } createUser(token, realmConfig, systemUser) -# Grants a role to a user - -def addUserRole(user: dict, role: dict, options: dict): +# Grants a role to a user +def addUserRole(user: dict, role: list, options: dict): url = options['url'] + '/' + user['id'] + '/role-mappings/realm' try: - response = requests.post(url, verify=False, json=[ - {'id': role['id'], 'name':role['name']}], - headers=options['headers']) + for irole in role: + response = requests.post(url, verify=False, json=[{'id': irole['id'], 'name': irole['name']}], + headers=options['headers']) + if response.status_code >= 200 and response.status_code < 300: + print('User role', user['username'], irole['name'], 'created!') + else: + print('Creation of user role', user['username'], irole['name'], 'failed!\n', response.text) except requests.exceptions.Timeout: sys.exit('HTTP request failed, please check you internet connection.') except requests.exceptions.TooManyRedirects: @@ -242,28 +244,24 @@ def addUserRole(user: dict, role: dict, options: dict): # catastrophic error. bail. raise SystemExit(e) - if response.status_code >= 200 and response.status_code < 300: - print('User role', user['username'], role['name'], 'created!') - else: - print('Creation of user role', - user['username'], role['name'], 'failed!\n', response.text) # searches for the role of a given user - - def findRole(username: str, authConfig: dict, realmConfig: dict) -> dict: + roleList = [] + roleNames = [] roleName = 'administration' for grant in authConfig['grants']: if grant['username'] == username: roleName = grant['role'] - for role in realmConfig['roles']['realm']: - if role['name'] == roleName: - return role - return None - -# adds roles to users + roleNames = roleName.split(",") # A user can have multiple roles, comma separated + for iroleName in roleNames: + for role in realmConfig['roles']['realm']: + if role['name'] == iroleName: + roleList.append(role) + return roleList +# adds roles to users def addUserRoles(token, realmConfig, authConfig): realmId = realmConfig['id'] url = base + '/admin/realms/' + realmId + '/users' @@ -296,12 +294,12 @@ def addUserRoles(token, realmConfig, authConfig): else: sys.exit('Getting users failed.') -# main +# main -(realmFile, authFile, readyTimeout) = load_arguments(sys.argv) -username = get_environment_variable('ADMIN_USERNAME') -password = get_environment_variable('ADMIN_PASSWORD') +(realmFile, authFile, readyTimeout) = loadArgs(sys.argv) +username = get_env('ADMIN_USERNAME') +password = get_env('ADMIN_PASSWORD') base = getBaseUrl() isReady(readyTimeout) token = getToken() diff --git a/solution/smo/common/identity/themes/README.md b/solution/smo/common/identity/themes/README.md new file mode 100644 index 0000000..98d3823 --- /dev/null +++ b/solution/smo/common/identity/themes/README.md @@ -0,0 +1,6 @@ +# add themes to solution +- copy `org.keycloak.keycloak-themes-XX.Y.Z.jar` from image and unzip +- copy keycloak themes into directory a themes subdirectory directory with +- modify css and resources (see dev resoures in keycloak) +- use `- KEYCLOAK_EXTRA_ARGS="--log-level=DEBUG --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false"` for online development +- add `KEYCLOAK_EXTRA_ARGS="--spi-theme-default=5gberlin"` to as environment in docker-compose.yml for identity to select as default theme \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/README.md b/solution/smo/common/identity/themes/oam/README.md new file mode 100644 index 0000000..17e8a14 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/README.md @@ -0,0 +1,16 @@ +# create custom theme + +A detailed description of the theme creation can be found in the [keycloak documentation](https://www.keycloak.org/docs/latest/server_development/#_themes). +It's not necessary to create a new theme from scratch. You can inherit from a base theme and override only the necessary +parts. + +- use `- KEYCLOAK_EXTRA_ARGS="--log-level=DEBUG --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false"` for theme development + +# add themes to solution + +After creating the theme, you can add it to the solution. The following steps are necessary: + +- mount the themes directory into the keycloak container + - target directory: `/opt/bitnami/keycloak/themes/[custom-theme-name]` +- add `KEYCLOAK_EXTRA_ARGS="--spi-theme-default=[custom-theme-name]"` to the environment section in docker-compose.yml + for identity to select as default theme \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/account/theme.properties b/solution/smo/common/identity/themes/oam/account/theme.properties new file mode 100644 index 0000000..12d9c19 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/account/theme.properties @@ -0,0 +1,2 @@ +parent=keycloak +import=common/keycloak \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/admin/theme.properties b/solution/smo/common/identity/themes/oam/admin/theme.properties new file mode 100644 index 0000000..12d9c19 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/admin/theme.properties @@ -0,0 +1,2 @@ +parent=keycloak +import=common/keycloak \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/email/theme.properties b/solution/smo/common/identity/themes/oam/email/theme.properties new file mode 100644 index 0000000..12d9c19 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/email/theme.properties @@ -0,0 +1,2 @@ +parent=keycloak +import=common/keycloak \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/login/resources/css/styles.css b/solution/smo/common/identity/themes/oam/login/resources/css/styles.css new file mode 100644 index 0000000..8cb33bc --- /dev/null +++ b/solution/smo/common/identity/themes/oam/login/resources/css/styles.css @@ -0,0 +1,19 @@ +.login-pf body { + background: DimGrey none; +} +.login-pf body { + background: url("../img/o-ran-sc-smo-oam-keyclock-background.png") no-repeat center center fixed; + background-size: cover; + height: 100%; +} +div.kc-logo-text { + background-image: url(../img/o-ran-sc-logo.png); + background-repeat: no-repeat; + height: 63px; + width: 300px; + margin: 0 auto; +} +.card-pf { + background: #bbb; + opacity: 0.8; +} \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-logo.png b/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-logo.png new file mode 100644 index 0000000..c3b6ce5 Binary files /dev/null and b/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-logo.png differ diff --git a/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-smo-oam-keyclock-background.png b/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-smo-oam-keyclock-background.png new file mode 100644 index 0000000..c8f24dd Binary files /dev/null and b/solution/smo/common/identity/themes/oam/login/resources/img/o-ran-sc-smo-oam-keyclock-background.png differ diff --git a/solution/smo/common/identity/themes/oam/login/theme.properties b/solution/smo/common/identity/themes/oam/login/theme.properties new file mode 100644 index 0000000..cad2a08 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/login/theme.properties @@ -0,0 +1,3 @@ +parent=keycloak +import=common/keycloak +styles=css/login.css css/styles.css \ No newline at end of file diff --git a/solution/smo/common/identity/themes/oam/welcome/theme.properties b/solution/smo/common/identity/themes/oam/welcome/theme.properties new file mode 100644 index 0000000..12d9c19 --- /dev/null +++ b/solution/smo/common/identity/themes/oam/welcome/theme.properties @@ -0,0 +1,2 @@ +parent=keycloak +import=common/keycloak \ No newline at end of file diff --git a/solution/smo/oam/.env b/solution/smo/oam/.env index 8541289..588a941 100644 --- a/solution/smo/oam/.env +++ b/solution/smo/oam/.env @@ -28,13 +28,14 @@ HTTP_DOMAIN=smo.o-ran-sc.org IDENTITY_PROVIDER_URL=https://identity.${HTTP_DOMAIN} # SDN Controller -SDNC_IMAGE=nexus3.onap.org:10001/onap/sdnc-image:2.4.2 +SDNC_IMAGE=nexus3.onap.org:10001/onap/sdnc-web-image:2.6.1 SDNC_REST_PORT=8181 +SDNR_WEBSOCKET_PORT=8182 SDNC_CERT_DIR=/opt/opendaylight/current/certs SDNC_ENABLE_OAUTH=true # SDN Controller Web -SDNC_WEB_IMAGE=nexus3.onap.org:10001/onap/sdnc-web-image:2.4.2 +SDNC_WEB_IMAGE=nexus3.onap.org:10001/onap/sdnc-web-image:2.6.1 SDNC_WEB_PORT=8080 ## VES Collector diff --git a/solution/smo/oam/controller/mountpoint-registrar.properties b/solution/smo/oam/controller/mountpoint-registrar.properties index 526c07a..7968841 100644 --- a/solution/smo/oam/controller/mountpoint-registrar.properties +++ b/solution/smo/oam/controller/mountpoint-registrar.properties @@ -4,35 +4,43 @@ baseUrl=http://localhost:8181 sdnrUser=admin sdnrPasswd=${ODL_ADMIN_PASSWORD} +[strimzi-kafka] +strimziEnabled=true +bootstrapServers=kafka:9092 +securityProtocol=PLAINTEXT +saslMechanism=PLAIN +saslJaasConfig=PLAIN + + [fault] -TransportType=HTTPNOAUTH -host=messages:3904 topic=unauthenticated.SEC_FAULT_OUTPUT -contenttype=application/json -group=myG -id=C1 -timeout=2000 -limit=1000 +consumerGroup=myG +consumerID=C1 +timeout=20000 +limit=10000 +fetchPause=5000 + +[provisioning] +topic=unauthenticated.SEC_3GPP_PROVISIONING_OUTPUT +consumerGroup=myG +consumerID=C1 +timeout=20000 +limit=10000 fetchPause=5000 [pnfRegistration] -TransportType=HTTPNOAUTH -host=messages:3904 topic=unauthenticated.VES_PNFREG_OUTPUT -contenttype=application/json -group=myG -id=C1 -timeout=2000 -limit=1000 +consumerGroup=myG +consumerID=C1 +timeout=20000 +limit=10000 fetchPause=5000 -[provisioning] -TransportType=HTTPNOAUTH -host=messages:3904 -topic=unauthenticated.SEC_3GPP_PROVISIONING_OUTPUT -contenttype=application/json -group=myG -id=C1 +[stndDefinedFault] +topic=unauthenticated.SEC_3GPP_FAULTSUPERVISION_OUTPUT +consumerGroup=myG +consumerID=C1 timeout=20000 limit=10000 -fetchPause=5000 \ No newline at end of file +fetchPause=5000 + diff --git a/solution/smo/oam/controller/oauth-aaa-app-config.xml b/solution/smo/oam/controller/oauth-aaa-app-config.xml index c210e37..8acb414 100644 --- a/solution/smo/oam/controller/oauth-aaa-app-config.xml +++ b/solution/smo/oam/controller/oauth-aaa-app-config.xml @@ -15,37 +15,34 @@ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - ~ ============LICENSE_END======================================================= - ~ - --> +~ See the License for the specific language governing permissions and +~ limitations under the License. +~ ============LICENSE_END======================================================= + ~ + --> - +
- tokenAuthRealm + tokenAuthRealm org.onap.ccsdk.features.sdnr.wt.oauthprovider.OAuth2Realm -
+
- securityManager.realms + securityManager.realms $tokenAuthRealm -
+ -
- authcBasic - org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter -
anyroles org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.AnyRoleHttpAuthenticationFilter -
+
- authcBearer - org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.BearerAndBasicHttpAuthenticationFilter -
+ authcBearer + + org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.BearerAndBasicHttpAuthenticationFilter +
@@ -53,31 +50,34 @@ org.opendaylight.aaa.shiro.filters.AuthenticationListener
- securityManager.authenticator.authenticationListeners - $accountingListener -
+ securityManager.authenticator.authenticationListeners + $accountingListener +
- dynamicAuthorization - org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter -
+ dynamicAuthorization + org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter + /**/operations/cluster-admin** - authcBearer, roles[admin] + authcBasic, roles[admin] /**/v1/** - authcBearer, roles[admin] + authcBasic, roles[admin] - - /rests/**/aaa*/** - authcBearer, roles[admin] + /rests/**/aaa*/** + authcBasic, roles[admin] + + + + /**/config/aaa*/** + authcBasic, roles[admin] - /oauth/** anon @@ -86,23 +86,29 @@ /ready anon - - /odlux/** - anon - - + /apidoc/** + authcBasic, roles[admin] + + - /apidoc/** + /rests/data/network-topology:network-topology authcBasic, roles[admin] - /rests/** - authcBearer, anyroles["admin,provision"] - - + /rests/operations/netconf-keystore* + authcBasic, roles[admin] + + + - /** - authcBearer, anyroles["admin,provision"] - + /rests/** + authcBearer, dynamicAuthorization + + + +/** +authcBearer, roles[admin] +
+ diff --git a/solution/smo/oam/docker-compose.yaml b/solution/smo/oam/docker-compose.yaml index 0042d22..bc827ae 100755 --- a/solution/smo/oam/docker-compose.yaml +++ b/solution/smo/oam/docker-compose.yaml @@ -15,8 +15,8 @@ # # no more versions needed! Compose spec supports all features w/o a version +version: "3.8" services: - odlux: image: ${SDNC_WEB_IMAGE} container_name: odlux @@ -28,12 +28,16 @@ services: SDNRPROTOCOL: http SDNRHOST: controller SDNRPORT: ${SDNC_REST_PORT} + SDNRWEBSOCKETPORT: ${SDNR_WEBSOCKET_PORT} labels: traefik.enable: true traefik.http.routers.sdnc-web.entrypoints: websecure traefik.http.routers.sdnc-web.rule: Host(`odlux.oam.${HTTP_DOMAIN}`) traefik.http.routers.sdnc-web.tls: true traefik.http.services.sdnc-web.loadbalancer.server.port: ${SDNC_WEB_PORT} + app: "odlux" + deploy: "o-ran-sc-smo-oam" + solution: "o-ran-sc-smo" depends_on: controller: condition: service_healthy @@ -56,6 +60,7 @@ services: environment: ENABLE_ODL_CLUSTER: false ENABLE_OAUTH: ${SDNC_ENABLE_OAUTH} + ENABLE_ODLUX_RBAC: false ODL_CERT_DIR: ${SDNC_CERT_DIR} ODL_ADMIN_PASSWORD: ${ADMIN_PASSWORD} SDNC_CONFIG_DIR: /opt/onap/ccsdk/data/properties @@ -66,10 +71,14 @@ services: SDNRONLY: true SDNRINIT: true SDNRDM: true - SDNRDBURL: http://persistence:9200 + SDNRDBTYPE: MARIADB + SDNRDBURL: jdbc:mysql://persistence:3306/sdnrdb + SDNRDBUSERNAME: sdnrdb + SDNRDBPASSWORD: sdnrdb SDNR_NETCONF_CALLHOME_ENABLED: true A1_ADAPTER_NORTHBOUND: false JAVA_OPTS: -Xms256m -Xmx4g + SDNR_WEBSOCKET_PORT: ${SDNR_WEBSOCKET_PORT} IDENTITY_PROVIDER_URL: ${IDENTITY_PROVIDER_URL} SDNC_WEB_URL: https://odlux.oam.${HTTP_DOMAIN} SDNR_VES_COLLECTOR_ENABLED: true @@ -107,6 +116,9 @@ services: traefik.tcp.routers.controller-tls.tls: false traefik.tcp.routers.controller-tls.service: controller-tls traefik.tcp.services.controller-tls.loadbalancer.server.port: 4335 + app: "controller" + deploy: "o-ran-sc-smo-oam" + solution: "o-ran-sc-smo" networks: smo: dcn: @@ -117,6 +129,7 @@ services: context: ./ves-collector args: - BASEIMAGE=${VES_COLLECTOR_IMAGE} + network: host container_name: ves-collector hostname: ves-collector extra_hosts: @@ -138,6 +151,9 @@ services: traefik.http.routers.ves.rule: Host(`ves-collector.dcn.${HTTP_DOMAIN}`) traefik.http.routers.ves.tls: true traefik.http.services.ves.loadbalancer.server.port: ${VES_ENDPOINT_PORT} + app: "ves-collector" + deploy: "o-ran-sc-smo-oam" + solution: "o-ran-sc-smo" networks: smo: dcn: diff --git a/solution/teardown.sh b/solution/teardown.sh new file mode 100755 index 0000000..0e5340a --- /dev/null +++ b/solution/teardown.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +docker compose -f $SCRIPT_DIR/smo/oam/docker-compose.yaml down +docker compose -f $SCRIPT_DIR/smo/common/docker-compose.yaml down diff --git a/solution/users.csv b/solution/users.csv new file mode 100644 index 0000000..a67f5e9 --- /dev/null +++ b/solution/users.csv @@ -0,0 +1,6 @@ +firstName,lastName,email,username,password,role,enabled,force_pwd_change +Leia,Organa,leia.organa@sdnr.onap.org,leia.organa,Default4SDN!,administration,true,false +R2,D2,r2.d2@sdnr.onap.org,r2.d2,Default4SDN!,administration,true,false +Luke,Skywalker,luke.skywalker@sdnr.onap.org,luke.skywalker,Default4SDN!,provision,true,false +Jargo,Fett,jargo.fett@sdnr.onap.org,jargo.fett,Default4SDN!,supervision,true,false +Martin,Skorupski,martin.skorupski@highstreet-technologies.com,martin.skorupski,Default4SDN!,administration,true,false \ No newline at end of file