Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move to ASGI using Daphne #2941

Open
FroggyFlox opened this issue Dec 20, 2024 · 5 comments
Open

Move to ASGI using Daphne #2941

FroggyFlox opened this issue Dec 20, 2024 · 5 comments

Comments

@FroggyFlox
Copy link
Member

FroggyFlox commented Dec 20, 2024

Building upon the recent hard work moving us to modern Python and Django, we can now open new possibilities for Rockstor for more dynamic behaviors and overall better user experience. Some of these would rely on async code, however, which means we need to move to serving our Django app with a compatible server: one that supports ASGI.

Following Django's documentation (https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/), we have several options:

  • Daphne
  • Hypercorn
  • Uvicorn

Out of these, Daphne seems to be a preferred choice overall, as it has been created "by and for" Django (Django channels, I believe), and is currently actively maintained by Django:
https://github.com/django/daphne

https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/daphne/

Note this might be preferred to be accomplished after #2650.

This move would also be a requirement for #2739.

@FroggyFlox
Copy link
Member Author

For reference, the asgi.py file automatically generated by Django 4.2 for a new project is as follows:

# asgi.py
"""
ASGI config for test_django_project project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_django_project.settings')

application = get_asgi_application()

@FroggyFlox
Copy link
Member Author

FroggyFlox commented Jan 1, 2025

Poetry install

buildvm:/opt/rockstor # poetry add -vv daphne
Using virtualenv: /opt/rockstor/.venv
Using version ^4.1.2 for daphne

Updating dependencies
Resolving dependencies...
(...)
   1: derived: daphne (>=4.1.2,<5.0.0)
(...)
   1: fact: daphne (4.1.2) depends on asgiref (>=3.5.2,<4)
   1: fact: daphne (4.1.2) depends on autobahn (>=22.4.2)
   1: fact: daphne (4.1.2) depends on twisted (>=22.4)
   1: selecting daphne (4.1.2)
(...)
   1: Version solving took 0.292 seconds.
   1: Tried 1 solutions.

Finding the necessary packages for the current system

Package operations: 13 installs, 0 updates, 0 removals, 52 skipped

  • Installing pyasn1 (0.6.1)
  • Installing attrs (24.3.0)
  • Installing pyasn1-modules (0.4.1)
  • Installing automat (24.8.1)
  • Installing constantly (23.10.4)
  • Installing hyperlink (21.0.0)
  • Installing incremental (24.7.2)
  • Installing pyopenssl (24.3.0)
  • Installing service-identity (24.2.0)
  • Installing txaio (23.1.1)
  • Installing autobahn (24.4.2)
  • Installing twisted (24.11.0)
  • Installing daphne (4.1.2)

Writing lock file
buildvm:/opt/rockstor # poetry show daphne --tree
daphne 4.1.2 Django ASGI (HTTP/WebSocket) server
├── asgiref >=3.5.2,<4
├── autobahn >=22.4.2
│   ├── cryptography >=3.4.6 
│   │   └── cffi >=1.12 
│   │       └── pycparser * 
│   ├── hyperlink >=21.0.0 
│   │   └── idna >=2.5 
│   ├── setuptools * 
│   └── txaio >=21.2.1 
└── twisted >=22.4
    ├── attrs >=22.2.0 
    ├── automat >=24.8.0 
    ├── constantly >=15.1 
    ├── hyperlink >=17.1.1 
    │   └── idna >=2.5 
    ├── idna >=2.4 (circular dependency aborted here)
    ├── incremental >=24.7.0 
    │   └── setuptools >=61.0 
    ├── pyopenssl >=21.0.0 
    │   └── cryptography >=41.0.5,<45 
    │       └── cffi >=1.12 
    │           └── pycparser * 
    ├── service-identity >=18.1.0 
    │   ├── attrs >=19.1.0 (circular dependency aborted here)
    │   ├── cryptography * (circular dependency aborted here)
    │   ├── pyasn1 * 
    │   └── pyasn1-modules * 
    │       └── pyasn1 >=0.4.6,<0.7.0 (circular dependency aborted here)
    ├── typing-extensions >=4.2.0 
    └── zope-interface >=5 
        └── setuptools * (circular dependency aborted here)

Daphne command

The install results in a binary available in our Poetry env:

buildvm:/opt/rockstor # poetry run daphne --help
usage: daphne [-h] [-p PORT] [-b HOST] [--websocket_timeout WEBSOCKET_TIMEOUT] [--websocket_connect_timeout WEBSOCKET_CONNECT_TIMEOUT] [-u UNIX_SOCKET] [--fd FILE_DESCRIPTOR] [-e SOCKET_STRINGS] [-v VERBOSITY] [-t HTTP_TIMEOUT] [--access-log ACCESS_LOG]
              [--log-fmt LOG_FMT] [--ping-interval PING_INTERVAL] [--ping-timeout PING_TIMEOUT] [--application-close-timeout APPLICATION_CLOSE_TIMEOUT] [--root-path ROOT_PATH] [--proxy-headers] [--proxy-headers-host PROXY_HEADERS_HOST]
              [--proxy-headers-port PROXY_HEADERS_PORT] [-s SERVER_NAME] [--no-server-name]
              application

Django HTTP/WebSocket server

positional arguments:
  application           The application to dispatch to as path.to.module:instance.path

options:
  -h, --help            show this help message and exit
  -p PORT, --port PORT  Port number to listen on
  -b HOST, --bind HOST  The host/address to bind to
  --websocket_timeout WEBSOCKET_TIMEOUT
                        Maximum time to allow a websocket to be connected. -1 for infinite.
  --websocket_connect_timeout WEBSOCKET_CONNECT_TIMEOUT
                        Maximum time to allow a connection to handshake. -1 for infinite
  -u UNIX_SOCKET, --unix-socket UNIX_SOCKET
                        Bind to a UNIX socket rather than a TCP host/port
  --fd FILE_DESCRIPTOR  Bind to a file descriptor rather than a TCP host/port or named unix socket
  -e SOCKET_STRINGS, --endpoint SOCKET_STRINGS
                        Use raw server strings passed directly to twisted
  -v VERBOSITY, --verbosity VERBOSITY
                        How verbose to make the output
  -t HTTP_TIMEOUT, --http-timeout HTTP_TIMEOUT
                        How long to wait for worker before timing out HTTP connections
  --access-log ACCESS_LOG
                        Where to write the access log (- for stdout, the default for verbosity=1)
  --log-fmt LOG_FMT     Log format to use
  --ping-interval PING_INTERVAL
                        The number of seconds a WebSocket must be idle before a keepalive ping is sent
  --ping-timeout PING_TIMEOUT
                        The number of seconds before a WebSocket is closed if no response to a keepalive ping
  --application-close-timeout APPLICATION_CLOSE_TIMEOUT
                        The number of seconds an ASGI application has to exit after client disconnect before it is killed
  --root-path ROOT_PATH
                        The setting for the ASGI root_path variable
  --proxy-headers       Enable parsing and using of X-Forwarded-For and X-Forwarded-Port headers and using that as the client address
  --proxy-headers-host PROXY_HEADERS_HOST
                        Specify which header will be used for getting the host part. Can be omitted, requires --proxy-headers to be specified when passed. "X-Real-IP" (when passed by your webserver) is a good candidate for this.
  --proxy-headers-port PROXY_HEADERS_PORT
                        Specify which header will be used for getting the port part. Can be omitted, requires --proxy-headers to be specified when passed.
  -s SERVER_NAME, --server-name SERVER_NAME
                        specify which value should be passed to response header Server attribute
  --no-server-name

@FroggyFlox
Copy link
Member Author

Very crude replacement of gunicorn in supervisord.conf:

; Daphne
[program:daphne]
command=/opt/rockstor/.venv/bin/daphne -b 127.0.0.1 -p 8000 asgi:application
process_name=%(program_name)s ; process_name expr (default %(program_name)s)
numprocs=1                    ; number of processes copies to start (def 1)
priority=100
autostart=true                ; start at supervisord start (default: true)
autorestart=unexpected        ; whether/when to restart (default: unexpected)
startsecs=5                   ; number of secs prog must stay running (def. 1)
startretries=3                ; max # of serial start failures (default 3)
exitcodes=0,2                 ; 'expected' exit codes for process (default 0,2)
stopsignal=TERM               ; signal used to kill process (default TERM)
stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
stopasgroup=false             ; send stop signal to the UNIX process group (default false)
killasgroup=false             ; SIGKILL the UNIX process group (def false)
stdout_logfile=/opt/rockstor/var/log/supervisord_%(program_name)s_stdout.log        ; stdout log path, NONE for none; default AUTO
stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
stdout_logfile_backups=10     ; # of stdout logfile backups (default 10)
stderr_logfile=/opt/rockstor/var/log/supervisord_%(program_name)s_stderr.log        ; stderr log path, NONE for none; default AUTO
stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
stderr_logfile_backups=10     ; # of stderr logfile backups (default 10)
stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)

Re-build, and we see in the daphne logs (interestingly seems to only write to stderr with these settings):

2025-01-01 14:04:17,676 INFO     Starting server at tcp:port=8000:interface=127.0.0.1
2025-01-01 14:04:17,676 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2025-01-01 14:04:17,676 INFO     Configuring endpoint tcp:port=8000:interface=127.0.0.1
2025-01-01 14:04:17,677 INFO     Listening on TCP address 127.0.0.1:8000
2025-01-01 14:04:18,709 INFO     No "rockstor" package found: source install?
2025-01-01 14:04:18,992 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda', 'model': 'QEMU HARDDISK', 'serial': 'aaaOS1', 'size': '12G', 'transport': 'sata', 'vendor': 'ATA', 'hctl': '2:0:0:0', 'type': 'disk', 'fstype': None, 'label': None, 'uuid': None
}
2025-01-01 14:04:18,993 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda1', 'model': None, 'serial': None, 'size': '2M', 'transport': None, 'vendor': None, 'hctl': None, 'type': 'part', 'fstype': None, 'label': None, 'uuid': None}
2025-01-01 14:04:18,993 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda2', 'model': None, 'serial': None, 'size': '64M', 'transport': None, 'vendor': None, 'hctl': None, 'type': 'part', 'fstype': 'vfat', 'label': 'EFI', 'uuid': '8310-69C2'}
2025-01-01 14:04:18,993 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda3', 'model': None, 'serial': None, 'size': '2G', 'transport': None, 'vendor': None, 'hctl': None, 'type': 'part', 'fstype': 'swap', 'label': 'SWAP', 'uuid': 'fd4aac37-32c2-499f-a8e
1-b04f4850c884'}
2025-01-01 14:04:18,993 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda4', 'model': None, 'serial': None, 'size': '9.9G', 'transport': None, 'vendor': None, 'hctl': None, 'type': 'part', 'fstype': 'btrfs', 'label': 'ROOT', 'uuid': 'bc2b87f2-97da-4078-
be56-a8b15072a3ac'}
2025-01-01 14:04:18,994 DEBUG    (/dev/sda) is parent of partition (/dev/sda4): backporting info to parent.
2025-01-01 14:04:18,994 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sdb', 'model': 'QEMU HARDDISK', 'serial': 'bbbData1', 'size': '5G', 'transport': 'sata', 'vendor': 'ATA', 'hctl': '3:0:0:0', 'type': 'disk', 'fstype': 'btrfs', 'label': 'main_pool', '
uuid': '900e4fca-a18a-4dca-8350-4bb90e88c466'}
2025-01-01 14:04:18,994 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sr0', 'model': 'QEMU DVD-ROM', 'serial': 'QM00001', 'size': '850.6M', 'transport': 'sata', 'vendor': 'QEMU', 'hctl': '1:0:0:0', 'type': 'rom', 'fstype': 'iso9660', 'label': 'INSTALL',
 'uuid': '2024-11-26-17-52-58-00'}
2025-01-01 14:04:19,141 DEBUG    Bootstrap operations completed
(...)

Open browser to webUI:

==> var/log/supervisord_daphne_stderr.log <==
2025-01-01 14:08:53,589 DEBUG    context={'request': <ASGIRequest: GET '/'>, 'current_appliance': <Appliance: Appliance object (1)>, 'setup_user': True, 'page_size': 15, 'update_channel': 'Testing'}
Session data corrupted
2025-01-01 14:08:53,591 WARNING  Session data corrupted

==> var/log/rockstor.log <==
[01/Jan/2025 14:08:53] DEBUG [storageadmin.views.home:73] ABOUT TO RENDER LOGIN

==> var/log/supervisord_daphne_stderr.log <==
2025-01-01 14:08:53,591 DEBUG    ABOUT TO RENDER LOGIN

==> var/log/supervisord_data-collector_stderr.log <==
::ffff:127.0.0.1 - - [2025-01-01 14:08:53] "GET /socket.io/?EIO=4&transport=polling&t=PGZHLGx HTTP/1.1" 200 254 0.000617
::ffff:127.0.0.1 - - [2025-01-01 14:08:53] "POST /socket.io/?EIO=4&transport=polling&t=PGZHLHV&sid=0UHAWbtxXJodBZCcAAAC HTTP/1.1" 200 189 0.001397
::ffff:127.0.0.1 - - [2025-01-01 14:08:53] "GET /socket.io/?EIO=4&transport=polling&t=PGZHLHV.0&sid=0UHAWbtxXJodBZCcAAAC HTTP/1.1" 200 327 0.001074
::ffff:127.0.0.1 - - [2025-01-01 14:08:53] "GET /socket.io/?EIO=4&transport=polling&t=PGZHLHo&sid=0UHAWbtxXJodBZCcAAAC HTTP/1.1" 200 157 0.000321

First thing we notice is that the daphne logs seem to duplicate rockstor.log to some extent.
Note also that data_collector seems to run OK still.
Otherwise, it seems to behave as expected so far, we can confirm we are being served under ASGI from the context debug line:

==> var/log/supervisord_daphne_stderr.log <==
2025-01-01 14:08:53,589 DEBUG    context={'request': <ASGIRequest: GET '/'>, 'current_appliance': <Appliance: Appliance object (1)>, 'setup_user': True, 'page_size': 15, 'update_channel': 'Testing'}

Although the submission of the sign-up form itself goes well, nothing seems to happen past that. Looking at Firefox's developer tools, we can see that:

  • the user setup POST goes OK
  • the login POST goes OK
  • then, there is a subsequent api/network/refresh request that fails and block everything else. It fails with 403 Forbidden.

My best guess so far is something related to headers that is not properly transmitted.

@FroggyFlox
Copy link
Member Author

Daphne was originally developed for Django Channels, so we can find some additional documentation there. In particular, there is some guide on deploying using Daphne controlled by Supervisord behind Nginx:
https://channels.readthedocs.io/en/latest/deploying.html#nginx-supervisor-ubuntu

@FroggyFlox
Copy link
Member Author

The answer seems to be in the headers, indeed.
Following the guide laid out at https://channels.readthedocs.io/en/latest/deploying.html#nginx-supervisor-ubuntu, I made the following changes:

First, add the --proxy-headers option to the daphne command. Based on that command's help, it does the following:

--proxy-headers Enable parsing and using of X-Forwarded-For and X-Forwarded-Port headers and using that as the client address

The X-Forwarded-For instruction is one that is currently not declared in our nginx conf. Comparing our current conf and the one listed in the Django Channels guide, we see 4 instructions in the guide that are NOT in our conf:

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;

Out of these 4, adding just one (X-Forwarded-For) makes everything seemingly work; see below:

		location / {
			proxy_pass_header Server;
			proxy_set_header Host $http_host;
			proxy_set_header X-Forwarded-Proto https;
			proxy_redirect off;
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header X-Scheme $scheme;
			proxy_connect_timeout 75;
			proxy_read_timeout 120;
			proxy_pass http://127.0.0.1:8000/;
            
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		}

In the Daphne log, we now see all seems to be fine:

2025-01-02 11:16:13,656 INFO     Starting server at tcp:port=8000:interface=127.0.0.1
2025-01-02 11:16:13,656 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2025-01-02 11:16:13,656 INFO     Configuring endpoint tcp:port=8000:interface=127.0.0.1
2025-01-02 11:16:13,657 INFO     Listening on TCP address 127.0.0.1:8000
2025-01-02 11:17:08,577 INFO     No "rockstor" package found: source install?
2025-01-02 11:17:08,633 DEBUG    context={'request': <ASGIRequest: GET '/'>, 'current_appliance': None, 'setup_user': False, 'page_size': 15, 'update_channel': 'Testing'}
2025-01-02 11:17:08,634 DEBUG    ABOUT TO RENDER SETUP
2025-01-02 11:17:20,279 DEBUG    Running command: /usr/sbin/usermod -p $6$McTVNx4DLp0z06A4$6Y9PWb0E7J4rv/uw7ThVpalPR317NB9nQlzm7TSWuLsAEX8ENJlZ3hUiI5z2B489d3gmPrMDqOpMcsq2VRljA0 radmin
2025-01-02 11:17:20,861 DEBUG    The function _update_or_create_ctype has been called
2025-01-02 11:17:20,864 DEBUG    The function _update_or_create_ctype has been called
2025-01-02 11:17:20,864 DEBUG    Unknown ctype: loopback config: {}
2025-01-02 11:17:20,994 DEBUG    The function _update_or_create_ctype has been called
2025-01-02 11:17:20,994 DEBUG    Unknown ctype: loopback config: {}
2025-01-02 11:17:20,996 DEBUG    The function _update_or_create_ctype has been called
2025-01-02 11:17:21,185 DEBUG    Scan_disks() using: blk_dev_properties={'name': '/dev/sda', 'model': 'QEMU HARDDISK', 'serial': 'aaaOS1', 'size': '12G', 'transport': 'sata', 'vendor': 'ATA', 'hctl': '2:0:0:0', 'type': 'disk', 'fstype': None, 'label': None, 'uuid': None}
(...)

According to Mozilla's docs, this header does the following:

When a client connects directly to a server, the client's IP address is sent to the server and is often written to server access logs. If a client connection passes through any forward or reverse proxies, the server only sees the final proxy's IP address, which is often of little use. That's especially true if the final proxy is a load balancer which is part of the same deployment as the server. To provide a more useful client IP address to the server, the X-Forwarded-For request header is used.

FroggyFlox added a commit to FroggyFlox/rockstor-core that referenced this issue Jan 2, 2025
This represents a work in progress and not suitable for use yet.
This commits includes first draft for minimum changes for Daphne to work
and all unit tests to still pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant