I'm trying to deploy the following Django-rest api on gcp ubuntu 22.0.4 using python 3.9.
https://github.com/OkunaOrg/okuna-api
The entire setup is supposed to be done and get setup using a single command :
python3.9 okuna-cli.py up-full
The execution seems stuck at "Waiting for server to come up..." and doesn't proceed ahead. The setup should complete by stating "Okuna is live at "domain". Another important aspect of the setup is the 5 docker containers are running and working fine when i run the py file. I'm even able to access the database after creating a superuser.
The code is as follows :
import random
import time
import click
import subprocess
import colorlog
import logging
import os.path
from shutil import copyfile
import json
import atexit
import os, errno
import requests
from halo import Halo
handler = colorlog.StreamHandler()
handler.setFormatter(colorlog.ColoredFormatter(
'%(log_color)s%(name)s -> %(message)s'))
logger = colorlog.getLogger('')
logger.addHandler(handler)
logger.setLevel(level=logging.DEBUG)
current_dir = os.path.dirname(__file__)
OKUNA_CLI_CONFIG_FILE = os.path.join(current_dir, '.okuna-cli.json')
OKUNA_CLI_CONFIG_FILE_TEMPLATE = os.path.join(current_dir, 'templates/.okuna-cli.json')
LOCAL_API_ENV_FILE = os.path.join(current_dir, '.env')
LOCAL_API_ENV_FILE_TEMPLATE = os.path.join(current_dir, 'templates/.env')
DOCKER_COMPOSE_ENV_FILE = os.path.join(current_dir, '.docker-compose.env')
DOCKER_COMPOSE_ENV_FILE_TEMPLATE = os.path.join(current_dir, 'templates/.docker-compose.env')
REQUIREMENTS_TXT_FILE = os.path.join(current_dir, 'requirements.txt')
DOCKER_API_IMAGE_REQUIREMENTS_TXT_FILE = os.path.join(current_dir, '.docker', 'api', 'requirements.txt')
DOCKER_WORKER_IMAGE_REQUIREMENTS_TXT_FILE = os.path.join(current_dir, '.docker', 'worker', 'requirements.txt')
DOCKER_SCHEDULER_IMAGE_REQUIREMENTS_TXT_FILE = os.path.join(current_dir, '.docker', 'scheduler', 'requirements.txt')
DOCKER_API_TEST_IMAGE_REQUIREMENTS_TXT_FILE = os.path.join(current_dir, '.docker', 'api-test', 'requirements.txt')
CONTEXT_SETTINGS = dict(
default_map={}
)
random_generator = random.SystemRandom()
def _remove_file_silently(filename):
try:
os.remove(filename)
except OSError as e: # this would be "except OSError, e:" before Python 2.6
if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory
raise # re-raise exception if a different error occurred
def _get_random_string(length=12,
allowed_chars='abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
"""
Return a securely generated random string.
The default length of 12 with the a-z, A-Z, 0-9 character set returns
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
"""
return ''.join(random.choice(allowed_chars) for i in range(length))
def _get_django_secret_key():
"""
Return a 50 character random string usable as a SECRET_KEY setting value.
"""
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
return _get_random_string(50, chars)
def _get_mysql_password():
return _get_random_string(64)
def _get_redis_password():
return _get_random_string(128)
def _copy_requirements_txt_to_docker_images_dir():
copyfile(REQUIREMENTS_TXT_FILE, DOCKER_API_IMAGE_REQUIREMENTS_TXT_FILE)
copyfile(REQUIREMENTS_TXT_FILE, DOCKER_WORKER_IMAGE_REQUIREMENTS_TXT_FILE)
copyfile(REQUIREMENTS_TXT_FILE, DOCKER_SCHEDULER_IMAGE_REQUIREMENTS_TXT_FILE)
def _check_okuna_api_is_running(address, port):
# Create a TCP socket
try:
response = requests.get('http://%s:%s/health/' % (address, port))
response_status = response.status_code
return response_status == 200
except requests.ConnectionError as e:
return False
def _wait_until_api_is_running(address, port, message='Waiting for server to come up...', sleep=None):
spinner = Halo(text=message, spinner='dots')
spinner.start()
if sleep:
time.sleep(sleep)
is_running = _check_okuna_api_is_running(address=address, port=port)
while not is_running:
is_running = _check_okuna_api_is_running(address=address, port=port)
spinner.stop()
def _clean():
"""
Cleans everything that the okuna-cli has created. Docker volumes, config files, everything.
:return:
"""
logger.info(' Cleaning up database')
subprocess.run(["docker", "volume", "rm", "okuna-api_mariadb"])
subprocess.run(["docker", "volume", "rm", "okuna-api_redisdb"])
logger.info(' Cleaning up config files')
_remove_file_silently(LOCAL_API_ENV_FILE)
_remove_file_silently(DOCKER_COMPOSE_ENV_FILE)
_remove_file_silently(OKUNA_CLI_CONFIG_FILE)
logger.info('✅ Clean up done!')
def _print_okuna_logo():
print(r"""
____ _
/ __ \| |
| | | | | ___ _ _ __ __ _
| | | | |/ | | | | '_ \ / _` |
| |__| | <| |_| | | | | (_| |
\____/|_|\_\\__,_|_| |_|\__,_|
""")
def _file_exists(filename):
return os.path.exists(filename) and os.path.isfile(filename)
def _replace_in_file(filename, texts):
with open(filename, 'r') as file:
filedata = file.read()
# Replace the target string
for key in texts:
value = texts[key]
filedata = filedata.replace(key, value)
# Write the file out again
with open(filename, 'w') as file:
file.write(filedata)
def _ensure_has_local_api_environment_file(okuna_cli_config):
if _file_exists(LOCAL_API_ENV_FILE):
return
logger.info('Local API .env file does not exist. Creating %s' % LOCAL_API_ENV_FILE)
if not _file_exists(LOCAL_API_ENV_FILE_TEMPLATE):
raise Exception('Local API .env file template did not exist')
copyfile(LOCAL_API_ENV_FILE_TEMPLATE, LOCAL_API_ENV_FILE)
_replace_in_file(LOCAL_API_ENV_FILE, {
"{{DJANGO_SECRET_KEY}}": okuna_cli_config['djangoSecretKey'],
"{{SQL_PASSWORD}}": okuna_cli_config['sqlPassword'],
"{{REDIS_PASSWORD}}": okuna_cli_config['redisPassword'],
})
def _ensure_has_docker_compose_api_environment_file(okuna_cli_config):
if _file_exists(DOCKER_COMPOSE_ENV_FILE):
return
logger.info('Docker compose env file does not exist. Creating %s' % DOCKER_COMPOSE_ENV_FILE)
if not _file_exists(DOCKER_COMPOSE_ENV_FILE_TEMPLATE):
raise Exception('Docker compose env file template did not exist')
copyfile(DOCKER_COMPOSE_ENV_FILE_TEMPLATE, DOCKER_COMPOSE_ENV_FILE)
_replace_in_file(DOCKER_COMPOSE_ENV_FILE, {
"{{DJANGO_SECRET_KEY}}": okuna_cli_config['djangoSecretKey'],
"{{SQL_PASSWORD}}": okuna_cli_config['sqlPassword'],
"{{REDIS_PASSWORD}}": okuna_cli_config['redisPassword'],
})
def _ensure_has_okuna_config_file():
if _file_exists(OKUNA_CLI_CONFIG_FILE):
return
django_secret_key = _get_django_secret_key()
mysql_password = _get_mysql_password()
redis_password = _get_redis_password()
logger.info('Generated DJANGO_SECRET_KEY=%s' % django_secret_key)
logger.info('Generated SQL_PASSWORD=%s' % mysql_password)
logger.info('Generated REDIS_PASSWORD=%s' % redis_password)
logger.info('Config file does not exist. Creating %s' % OKUNA_CLI_CONFIG_FILE)
if not _file_exists(OKUNA_CLI_CONFIG_FILE_TEMPLATE):
raise Exception('Config file template did not exists')
copyfile(OKUNA_CLI_CONFIG_FILE_TEMPLATE, OKUNA_CLI_CONFIG_FILE)
_replace_in_file(OKUNA_CLI_CONFIG_FILE, {
"{{DJANGO_SECRET_KEY}}": django_secret_key,
"{{SQL_PASSWORD}}": mysql_password,
"{{REDIS_PASSWORD}}": redis_password,
})
def _bootstrap(is_local_api):
logger.info(' Bootstrapping Okuna with some data')
if is_local_api:
subprocess.run(["./utils/scripts/bootstrap_development_data.sh"])
else:
subprocess.run(["docker-compose", "-f", "docker-compose-full.yml", "exec", "webserver",
"/bootstrap_development_data.sh"])
def _ensure_has_required_cli_config_files():
_ensure_has_okuna_config_file()
with open(OKUNA_CLI_CONFIG_FILE, 'r+') as okuna_cli_config_file:
okuna_cli_config = json.load(okuna_cli_config_file)
_ensure_has_docker_compose_api_environment_file(okuna_cli_config=okuna_cli_config)
_ensure_has_local_api_environment_file(okuna_cli_config=okuna_cli_config)
def _ensure_was_bootstrapped(is_local_api):
with open(OKUNA_CLI_CONFIG_FILE, 'r+') as okuna_cli_config_file:
okuna_cli_config = json.load(okuna_cli_config_file)
if okuna_cli_config['bootstrapped']:
return
logger.info('Okuna was not bootstrapped.')
_bootstrap(is_local_api=is_local_api)
okuna_cli_config['bootstrapped'] = True
okuna_cli_config_file.seek(0)
json.dump(okuna_cli_config, okuna_cli_config_file, indent=4)
okuna_cli_config_file.truncate()
logger.info('Okuna was bootstrapped.')
@click.group()
def cli():
pass
def _down_test():
"""Bring Okuna down"""
logger.error('⬇️ Bringing the Okuna test services down...')
subprocess.run(["docker-compose", "-f", "docker-compose-test-services-only.yml", "down"])
def _down_full():
"""Bring Okuna down"""
logger.error('⬇️ Bringing the whole of Okuna down...')
subprocess.run(["docker-compose", "-f", "docker-compose-full.yml", "down"])
def _down_services_only():
"""Bring Okuna down"""
logger.error('⬇️ Bringing the Okuna services down...')
subprocess.run(["docker-compose", "-f", "docker-compose-services-only.yml", "down"])
@click.command()
def down_services_only():
_down_services_only()
@click.command()
def down_full():
_down_full()
@click.command()
def up_full():
"""Bring the whole of Okuna up"""
_print_okuna_logo()
_ensure_has_required_cli_config_files()
_copy_requirements_txt_to_docker_images_dir()
logger.info('⬆️ Bringing the whole of Okuna up...')
atexit.register(_down_full)
subprocess.run(["docker-compose", "-f", "docker-compose-full.yml", "up", "-d", "-V"])
okuna_api_address = 'domain'
okuna_api_port = 80
_wait_until_api_is_running(address=okuna_api_address, port=okuna_api_port)
_ensure_was_bootstrapped(is_local_api=False)
logger.info(' Okuna is live at http://%s:%s.' % (okuna_api_address, okuna_api_port))
subprocess.run(["docker-compose", "-f", "docker-compose-full.yml", "logs", "--follow", "--tail=0", "webserver"])
input()
@click.command()
def up_services_only():
"""Bring only the Okuna services up. API is up to you."""
_print_okuna_logo()
_ensure_has_required_cli_config_files()
_copy_requirements_txt_to_docker_images_dir()
logger.info('⬆️ Bringing only the Okuna services up...')
atexit.register(_down_services_only)
subprocess.run(["docker-compose", "-f", "docker-compose-services-only.yml", "up", "-d", "-V"])
_ensure_was_bootstrapped(is_local_api=True)
logger.info(' Okuna services are up')
subprocess.run(["docker-compose", "-f", "docker-compose-services-only.yml", "logs", "--follow"])
input()
@click.command()
def down_test():
_down_test()
@click.command()
def up_test():
"""Bring the Okuna test services up"""
_print_okuna_logo()
_ensure_has_required_cli_config_files()
logger.info('⬆️ Bringing the Okuna test services up...')
atexit.register(_down_test)
subprocess.run(["docker-compose", "-f", "docker-compose-test-services-only.yml", "up", "-d", "-V"])
logger.info(' Okuna tests services are live')
subprocess.run(
["docker-compose", "-f", "docker-compose-test-services-only.yml", "logs", "--follow", "--tail=0"])
input()
@click.command()
def build_full():
"""Rebuild Okuna services"""
_ensure_has_required_cli_config_files()
logger.info('♀️ Rebuilding Okuna full services...')
_copy_requirements_txt_to_docker_images_dir()
subprocess.run(["docker-compose", "-f", "docker-compose-full.yml", "build"])
@click.command()
def build_services_only():
"""Rebuild Okuna services"""
_ensure_has_required_cli_config_files()
logger.info('♀️ Rebuilding only Okuna services...')
_copy_requirements_txt_to_docker_images_dir()
subprocess.run(["docker-compose", "-f", "docker-compose-services-only.yml", "build"])
@click.command()
def status():
"""Get Okuna status"""
logger.info('️♂️ Retrieving services status...')
subprocess.run(["docker-compose", "ps"])
@click.command()
def clean():
"""Bootstrap Okuna"""
_clean()
cli.add_command(up_full)
cli.add_command(down_full)
cli.add_command(up_test)
cli.add_command(down_test)
cli.add_command(up_services_only)
cli.add_command(down_services_only)
cli.add_command(build_full)
cli.add_command(build_services_only)
cli.add_command(clean)
cli.add_command(status)
if __name__ == '__main__':
cli()
I checked that the def status() isn't working as well which is supposed to check the running docker containers as defined in docker-compose.env and show results. I can se following error when I try:
python3.9 okuna-cli.py status
Can't find a suitable configuration file in this directory or any parent.Are you in the right directory?Supported filenames: docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml
When i do docker-compose -f docker-compose-full.yml up
I have the following warning displayed :
Aborted connection 3 to db: 'unconnected' user: 'unauthenticated' host: '172.16.16.2' (This connection closed normally without authentication)
Aborted connection 4 to db: 'unconnected' user: 'unauthenticated' host: '172.16.16.3' (This connection closed normally without authentication)
EDIT : The above Warning disappears after downgrading Mariadb version to 10.2
I'm getting the 2 additional warnings as well. This is despite running inside a virtual environment and I've done everything using only pip3 without sudo:
1.The directory '/root/.cache/pip' or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions and owner of that directory. If executing pip with sudo, you should use sudo's -H flag
2.Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
My docker-compose:
version: '3'
services:
webserver:
container_name: okuna-api
build:
dockerfile: Dockerfile
context: ./.docker/api
privileged: true
extra_hosts:
- db.okuna:172.16.16.4
- redis.okuna:172.16.16.5
volumes:
- ./:/opt/okuna-api-core
- ./.docker-cache/pip:/root/.cache/pip
ports:
- 80:80
working_dir: /opt/okuna-api-core
networks:
okuna:
ipv4_address: 172.16.16.1
depends_on:
- db
- redis
env_file:
- .docker-compose.env
worker:
container_name: okuna-worker
build:
dockerfile: Dockerfile
context: ./.docker/worker
privileged: true
extra_hosts:
- db.okuna:172.16.16.4
- redis.okuna:172.16.16.5
volumes:
- ./:/opt/okuna-api-core
- ./.docker-cache/pip:/root/.cache/pip
working_dir: /opt/okuna-api-core
networks:
okuna:
ipv4_address: 172.16.16.2
depends_on:
- webserver
env_file:
- .docker-compose.env
scheduler:
container_name: okuna-scheduler
build:
dockerfile: Dockerfile
context: ./.docker/scheduler
privileged: true
extra_hosts:
- db.okuna:172.16.16.4
- redis.okuna:172.16.16.5
volumes:
- ./:/opt/okuna-api-core
- ./.docker-cache/pip:/root/.cache/pip
working_dir: /opt/okuna-api-core
networks:
okuna:
ipv4_address: 172.16.16.3
depends_on:
- webserver
env_file:
- .docker-compose.env
db:
image: mariadb:latest
hostname: db.okuna
volumes:
- mariadb:/var/lib/mysql
ports:
- 3306
privileged: false
networks:
okuna:
ipv4_address: 172.16.16.4
command: --character-set-server=utf8 --collation-server=utf8_unicode_ci
env_file:
- .docker-compose.env
redis:
image: bitnami/redis:latest
privileged: false
ports:
- 6379
networks:
okuna:
ipv4_address: 172.16.16.5
env_file:
- .docker-compose.env
volumes:
- redisdb:/bitnami/redis/data
volumes:
mariadb:
redisdb:
networks:
okuna:
ipam:
driver: default
config:
- subnet: "172.16.16.0/16"
my docker-compose.env :
# Variable specifying execution environment
# Required always.
# Possible values: production,development,acceptance, test
ENVIRONMENT=development
# ============= START NON-ENV SPECIFIC VARIABLES ============= #
# [NAME] ALLOWED_HOSTS
# [DESCRIPTION] Django variable specifying allowed hosts.
# [REQUIRED][PRODUCTION]
# [MORE] https://docs.djangoproject.com/en/2.1/ref/settings/#allowed-hosts
#ALLOWED_HOSTS=www.openbook.social
# [NAME] SECRET_KEY
# [DESCRIPTION] Django variable to provide cryptographic signing. If using okuna-cli, obtained from .okuna-cli.json
# [REQUIRED][ALWAYS]
# [MORE] https://docs.djangoproject.com/en/2.1/ref/settings/#secret-key
SECRET_KEY=949m="long passwrod generated here"
# [NAME] JWT_ALGORITHM
# [DESCRIPTION] Django variable to provide cryptographic signing.
# [REQUIRED][ALWAYS]
# [MORE] https://docs.djangoproject.com/en/2.1/ref/settings/#secret-key
JWT_ALGORITHM=HS256
# [NAME] MEDIA_ROOT
# [DESCRIPTION] Absolute filesystem path to the directory that will hold user-uploaded files.
# [MORE] https://docs.djangoproject.com/en/2.1/ref/settings/#media-root
# [OPTIONAL=./media]
# MEDIA_ROOT=
# [NAME] MEDIA_URL
# [DESCRIPTION] URL that handles the media served from MEDIA_ROOT, used for managing stored files. It must end in a slash if set
# [MORE] https://docs.djangoproject.com/en/2.1/ref/settings/#media-url
# [OPTIONAL=/media/]
# MEDIA_URL=
# [GROUP] SQL Database Configuration
# [DESCRIPTION] The SQL database configuration
# [REQUIRED][ALWAYS]
RDS_DB_NAME=okuna
RDS_USERNAME=root
RDS_HOSTNAME=db.okuna
RDS_PORT=3306
RDS_HOSTNAME_READER=db.okuna
RDS_HOSTNAME_WRITER=db.okuna
#[NAME] RDS_PASSWORD
# [DESCRIPTION] The password for the SQL Database. If using okuna-cli, obtained from .okuna-cli.json
RDS_PASSWORD=long passwrod generated here
# [GROUP] Redis Database configuration Configuration
# [DESCRIPTION] The redis database configuration
# [REQUIRED][ALWAYS]
REDIS_HOST=redis.okuna
REDIS_PORT=6379
#[NAME] REDIS_PASSSWORD
# [DESCRIPTION] The password for the REDIS Database.
REDIS_PASSWORD=long password generated here
# [GROUP] Top posts criteria
# [DESCRIPTION] The criteria under which posts will be added to the Explore/Top posts section of the app
# [OPTIONAL=2]
# MIN_UNIQUE_TOP_POST_REACTIONS_COUNT=
# MIN_UNIQUE_TOP_POST_COMMENTS_COUNT=
# [NAME] NEW_USER_SUGGESTED_COMMUNITIES
# [DESCRIPTION] The ids of the communities to be suggested to a new user
# [OPTIONAL=1]
# NEW_USER_SUGGESTED_COMMUNITIES=1,1310,216
# [GROUP] Allowed media sizes
# [DESCRIPTION] The criteria under which posts will be added to the Explore/Top posts section of the app
# [OPTIONAL]
# POST_MEDIA_MAX_SIZE=30485760
# PROFILE_AVATAR_MAX_SIZE=10485760
# PROFILE_COVER_MAX_SIZE=10485760
# COMMUNITY_AVATAR_MAX_SIZE=10485760
# COMMUNITY_COVER_MAX_SIZE=10485760
# [NAME] MODERATORS_COMMUNITY_NAME
# [DESCRIPTION] The community which when joined, will become global moderators
# [OPTIONAL=mods]
# MODERATORS_COMMUNITY_NAME=
# ============= END NON-ENV SPECIFIC VARIABLES ============= #
# ============= START DOCKER COMPOSE SPECIFIC VARIABLES ============= #
# [GROUP] Mysql Docker Image env vars
# [DESCRIPTION] This must match the RDS_PASSWORD AND RDS_DATABASE env vars on top
# [REQUIRED][ALWAYS]
MYSQL_ROOT_PASSWORD=long password generated here
MYSQL_DATABASE=okuna
# [NAME] WAIT_HOSTS
# [DESCRIPTION] The hosts that the Kosmos API should wait for
# [REQUIRED]
WAIT_HOSTS:db.okuna:3306
# ============= END DOCKER COMPOSE SPECIFIC VARIABLES ============= #
This is despite the configuration files are intact and in the right place. Help appreciated.