```
$ cat /etc/hosts
127.0.0.1 localhost
-127.0.1.1 <your-system>
+127.0.1.1 10.20.35.165
# SMO OAM development system
<deployment-system-ipv4> smo.o-ran-sc.org
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
import os
import argparse
+from jinja2 import Template
default_ip_address = 'aaa.bbb.ccc.ddd'
default_http_domain = 'smo.o-ran-sc.org'
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):
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
# 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)
+
--- /dev/null
+#!/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
--- /dev/null
+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)
--- /dev/null
+#!/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
+
+
+
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
# 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}
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:
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
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}
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}
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:
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:
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:
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
{
- "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
#!/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.
#
# 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
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 = {
'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:
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
'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:
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
# 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'
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)
{
"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:
# 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'
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()
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+parent=keycloak
+import=common/keycloak
\ No newline at end of file
--- /dev/null
+parent=keycloak
+import=common/keycloak
\ No newline at end of file
--- /dev/null
+parent=keycloak
+import=common/keycloak
\ No newline at end of file
--- /dev/null
+.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
--- /dev/null
+parent=keycloak
+import=common/keycloak
+styles=css/login.css css/styles.css
\ No newline at end of file
--- /dev/null
+parent=keycloak
+import=common/keycloak
\ No newline at end of file
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
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
+
~ 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=======================================================
+ ~
+ -->
-<shiro-configuration xmlns="urn:opendaylight:aaa:app:config">
+ <shiro-configuration xmlns="urn:opendaylight:aaa:app:config">
<main>
- <pair-key>tokenAuthRealm</pair-key>
+ <pair-key>tokenAuthRealm</pair-key>
<pair-value>org.onap.ccsdk.features.sdnr.wt.oauthprovider.OAuth2Realm</pair-value>
- </main>
+ </main>
<main>
- <pair-key>securityManager.realms</pair-key>
+ <pair-key>securityManager.realms</pair-key>
<pair-value>$tokenAuthRealm</pair-value>
- </main>
+ </main>
<!-- Used to support OAuth2 use case. -->
- <main>
- <pair-key>authcBasic</pair-key>
- <pair-value>org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter</pair-value>
- </main>
<main>
<pair-key>anyroles</pair-key>
<pair-value>org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.AnyRoleHttpAuthenticationFilter</pair-value>
- </main>
+ </main>
<main>
- <pair-key>authcBearer</pair-key>
- <pair-value>org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.BearerAndBasicHttpAuthenticationFilter</pair-value>
- </main>
+ <pair-key>authcBearer</pair-key>
+ <!-- <pair-value>org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter</pair-value>-->
+ <pair-value>org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.BearerAndBasicHttpAuthenticationFilter</pair-value>
+ </main>
<!-- in order to track AAA challenge attempts -->
<main>
<pair-value>org.opendaylight.aaa.shiro.filters.AuthenticationListener</pair-value>
</main>
<main>
- <pair-key>securityManager.authenticator.authenticationListeners</pair-key>
- <pair-value>$accountingListener</pair-value>
- </main>
+ <pair-key>securityManager.authenticator.authenticationListeners</pair-key>
+ <pair-value>$accountingListener</pair-value>
+ </main>
<!-- Model based authorization scheme supporting RBAC for REST endpoints -->
<main>
- <pair-key>dynamicAuthorization</pair-key>
- <pair-value>org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter</pair-value>
- </main>
+ <pair-key>dynamicAuthorization</pair-key>
+ <pair-value>org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter</pair-value>
+ </main>
<urls>
<pair-key>/**/operations/cluster-admin**</pair-key>
- <pair-value>authcBearer, roles[admin]</pair-value>
+ <pair-value>authcBasic, roles[admin]</pair-value>
</urls>
<urls>
<pair-key>/**/v1/**</pair-key>
- <pair-value>authcBearer, roles[admin]</pair-value>
+ <pair-value>authcBasic, roles[admin]</pair-value>
</urls>
- <!-- allow admin only access to write mdsal auth config -->
<urls>
- <pair-key>/rests/**/aaa*/**</pair-key>
- <pair-value>authcBearer, roles[admin]</pair-value>
+ <pair-key>/rests/**/aaa*/**</pair-key>
+ <pair-value>authcBasic, roles[admin]</pair-value>
+ </urls>
+
+ <urls>
+ <pair-key>/**/config/aaa*/**</pair-key>
+ <pair-value>authcBasic, roles[admin]</pair-value>
</urls>
- <!-- anon access for login api -->
<urls>
<pair-key>/oauth/**</pair-key>
<pair-value>anon</pair-value>
<pair-key>/ready</pair-key>
<pair-value>anon</pair-value>
</urls>
- <!-- anon access for odlux ui -->
<urls>
- <pair-key>/odlux/**</pair-key>
- <pair-value>anon</pair-value>
- </urls>
- <!-- admin only access for apidocs -->
+ <pair-key>/apidoc/**</pair-key>
+ <pair-value>authcBasic, roles[admin]</pair-value>
+ </urls>
+ <!-- these two rules are needed for installCerts.py -->
<urls>
- <pair-key>/apidoc/**</pair-key>
+ <pair-key>/rests/data/network-topology:network-topology</pair-key>
<pair-value>authcBasic, roles[admin]</pair-value>
</urls>
<urls>
- <pair-key>/rests/**</pair-key>
- <pair-value>authcBearer, anyroles["admin,provision"]</pair-value>
- </urls>
- <!-- any other access with configured dynamic filter -->
+ <pair-key>/rests/operations/netconf-keystore*</pair-key>
+ <pair-value>authcBasic, roles[admin]</pair-value>
+ </urls>
+
+ <!-- rfc8040 restconf access with configured dynamic filter -->
<urls>
- <pair-key>/**</pair-key>
- <pair-value>authcBearer, anyroles["admin,provision"]</pair-value>
- </urls>
+ <pair-key>/rests/**</pair-key>
+ <pair-value>authcBearer, dynamicAuthorization</pair-value>
+ </urls>
+ <!-- any other access with configured dynamic filter -->
+<urls>
+<pair-key>/**</pair-key>
+<pair-value>authcBearer, roles[admin]</pair-value>
+</urls>
</shiro-configuration>
+
#
# 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
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
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
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
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:
context: ./ves-collector
args:
- BASEIMAGE=${VES_COLLECTOR_IMAGE}
+ network: host
container_name: ves-collector
hostname: ves-collector
extra_hosts:
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:
--- /dev/null
+#!/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
--- /dev/null
+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