diff --git a/thesis/source/docs/5_implementazione/index.rst b/thesis/source/docs/5_implementazione/index.rst index 60438ca..7e2198e 100644 --- a/thesis/source/docs/5_implementazione/index.rst +++ b/thesis/source/docs/5_implementazione/index.rst @@ -2,6 +2,1288 @@ Implementazione *************** -.. todo:: +.. todo:: Implementazione - Scrivere questo capitolo. +.. todo:: Create index + +.. todo:: Fix references + +.. todo:: Add details + +Implementazione del modulo backend +================================== + +.. todo:: Implementazione del modulo backend + + +Ambiente virtuale Python +------------------------ + +.. todo:: Ambiente virtuale Python + + +Il project Django +----------------- +.. default-domain:: py +.. default-role:: obj +.. module:: sophon + +Il project del modulo aggiunge alla struttura base autogenerata da Django alcune funzionalità utili all'intero backend. + + +Pagina di amministrazione personalizzata +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.admin + +La pagina di amministrazione viene personalizzata con la classe `SophonAdminSite`, che modifica alcuni parametri della classe base. + +Inoltre, il template predefinito viene sovrascritto da quello all'interno del file ``templates/admin/base.html``, che sostituisce il foglio di stile con uno personalizzato per Sophon. + +.. class:: SophonAdminSite(django.contrib.admin.AdminSite) + + .. attribute:: site_header = "Sophon Server Administration" + + Il nome della pagina nell'header viene modificato a *Sophon Server Administration*. + + .. attribute:: site_title = "Sophon Server Administration" + + Il titolo della pagina nell'header viene anch'esso modificato a *Sophon Server Administration*. + + .. attribute:: site_url = None + + Il collegamento *View Site* viene rimosso, in quanto è possibile accedere all'interfaccia web di Sophon da più domini contemporaneamente. + + .. attribute:: index_title = "Resources Administration" + + Il titolo dell'indice viene modificato a *Resources Administration*. + +.. class:: SophonAdminConfig(django.contrib.admin.apps.AdminConfig) + + .. attribute:: default_site = "sophon.admin.SophonAdminSite" + + `.SophonAdminSite` è selezionata come classe predefinita per il sito di amministrazione. + + +Impostazioni dinamiche +^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.settings + +Il file di impostazioni viene modificato per **permettere la configurazione attraverso variabili di ambiente** invece che attraverso il file ``settings.py``, rendendo il deployment con Docker molto più semplice. + +.. code-block:: python + + try: + DATABASE_ENGINE = os.environ["DJANGO_DATABASE_ENGINE"] + except KeyError: + log.warning("DJANGO_DATABASE_ENGINE was not set, defaulting to PostgreSQL") + DATABASE_ENGINE = "django.db.backends.postgresql" + log.debug(f"{DATABASE_ENGINE = }") + +Inoltre, viene configurato il modulo `logging` per emettere testo colorato di più facile comprensione usando il package `coloredlogs`. + +.. code-block:: python + + "detail": { + "()": coloredlogs.ColoredFormatter, + "format": "{asctime:>19} | {name:<24} | {levelname:>8} | {message}", + "style": "{", + } + + +Autenticazione migliorata +^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.auth1 + +La classe `rest_framework.authentication.TokenAuthentication` viene modificata per ottenere un comportamento conforme agli standard del web. + +.. class:: BearerTokenAuthentication(rest_framework.authentication.TokenAuthentication) + + .. attribute:: keyword = "Bearer" + + Si configura `rest_framework` per accettare header di autenticazione nella forma ``Bearer ``, invece che ``Token ``. + +.. module:: sophon.auth2 + +La view `rest_framework.authtoken.views.ObtainAuthToken` viene estesa per aggiungere dati alla risposta di autenticazione riuscita. + +.. class:: CustomObtainAuthToken(rest_framework.authtoken.views.ObtainAuthToken) + + .. method:: post(self, request, *args, **kwargs) + + In particolare, viene aggiunta una chiave ``user``, che contiene i dettagli sull'utente che ha effettuato il login. + + +L'app Sophon Core +----------------- +.. default-domain:: py +.. default-role:: obj +.. module:: sophon.core + +L'app `sophon.core` è l'app principale del progetto, e non può essere disattivata, in quanto dipendenza obbligatoria di tutte le altre app. + + +Aggiunta di un nuovo comando di gestione +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.core.management.commands.initsuperuser + +Per permettere l'integrazione la creazione automatica del primo :ref:`superutente` quando Sophon viene eseguito da Docker, viene introdotto dall'app il comando di gestione ``initsuperuser``. + +.. class:: Command + + Questo comando crea automaticamente un :ref:`superutente` con le credenziali specificate in :ref:`\`\`DJANGO_SU_USERNAME\`\``, :ref:`\`\`DJANGO_SU_EMAIL\`\`` e :ref:`\`\`DJANGO_SU_PASSWORD\`\``. + + +Modello base astratto +^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.core.models + +Viene estesa la classe astratta `django.db.models.Model` con funzioni per stabilire il livello di accesso di un utente all'oggetto e per generare automaticamente i `rest_framework.serializers.ModelSerializer` in base al livello di accesso. + +.. class:: SophonModel(django.db.models.Model) + + .. method:: can_edit(self, user: django.contrib.auth.models.User) -> bool + :abstractmethod: + + Controlla se un utente può modificare l'oggetto attuale. + + :param user: L'utente da controllare. + :returns: `True` se l'utente deve poter modificare l'oggetto, altrimenti `False`. + + .. method:: can_admin(self, user: django.contrib.auth.models.User) -> bool + :abstractmethod: + + Controlla se un utente può amministrare l'oggetto attuale. + + :param user: L'utente da controllare. + :returns: `True` se l'utente deve poter amministrare l'oggetto, altrimenti `False`. + + .. classmethod:: get_fields(cls) -> set[str] + + :returns: il `set` di nomi di campi che devono essere mostrati quando viene richiesto l'oggetto attraverso l'API. + + .. classmethod:: get_editable_fields(cls) -> set[str] + + :returns: il `set` di nomi di campi di cui deve essere permessa la modifica se l'utente può modificare (`.can_edit`) l'oggetto. + + .. classmethod:: get_administrable_fields(cls) -> set[str] + + :returns: il `set` di nomi di campi di cui deve essere permessa la modifica se l'utente può amministrare (`.can_admin`) l'oggetto. + + .. classmethod:: get_creation_fields(cls) -> set[str] + + :returns: il `set` di nomi di campi che possono essere specificati dall'utente al momento della creazione dell'oggetto. + + +Modello di autorizzazione astratto +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Viene definito un nuovo modello astratto, basato su `SophonModel`, che permette di determinare i permessi dell'utente in base alla sua appartenenza al gruppo a cui è collegato l'oggetto implementatore. + +.. class:: SophonGroupModel(SophonModel) + + .. method:: get_group(self) -> ResearchGroup + :abstractmethod: + + :returns: Il gruppo a cui appartiene l'oggetto. + + .. classmethod:: get_access_to_edit(cls) -> sophon.core.enums.SophonGroupAccess + + :returns: Il livello di autorità all'interno del gruppo necessario per modificare l'oggetto. + + .. classmethod:: get_access_to_admin(cls) -> sophon.core.enums.SophonGroupAccess + + :returns: Il livello di autorità all'interno del gruppo necessario per amministrare l'oggetto. + + .. method:: get_access_serializer(self, user: User) -> typing.Type[rest_framework.serializers.ModelSerializer] + + :returns: Restituisce il `rest_framework.serializers.ModelSerializer` adeguato al livello di autorità dell'utente. + + +.. class:: sophon.core.enums.SophonGroupAccess(enum.IntEnum) + + Enumerazione che stabilisce il livello di autorità che un utente può avere all'interno di un gruppo. + + .. attribute:: NONE = 0 + + Utente :ref:`ospite`. + + .. attribute:: REGISTERED = 10 + + :ref:`Utente` registrato. + + .. attribute:: MEMBER = 50 + + Membro del :ref:`gruppo di ricerca`. + + .. attribute:: OWNER = 100 + + Creatore del :ref:`gruppo di ricerca`. + + .. attribute:: SUPERUSER = 200 + + :ref:`Superutente` con privilegi universali. + + +Modello dei dettagli dell'istanza +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Viene creato il modello che rappresenta i dettagli dell':ref:`istanza` Sophon. + +.. class:: SophonInstanceDetails(SophonModel) + + .. attribute:: id: IntegerField [1] + + Impostando ``1`` come unica scelta per il campo della chiave primaria ``id``, si crea un modello "singleton", ovvero un modello di cui può esistere un'istanza sola in tutto il database. + + L'istanza unica viene creata dalla migrazione ``0004_sophoninstancedetails.py``. + + .. attribute:: name: CharField + .. attribute:: description: TextField + .. attribute:: theme: CharField ["sophon", "paper", "royalblue", "hacker", "amber"] + + .. method:: version: str + :property: + + :returns: La versione installata del pacchetto `sophon`. + + .. seealso:: + + :ref:`Sophon instance details` nella guida per l'amministratore. + + +Modello del gruppo di ricerca +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Viene creato il modello che rappresenta un :ref:`gruppo di ricerca`. + +.. class:: ResearchGroup(SophonGroupModel) + + .. attribute:: slug: SlugField + .. attribute:: name: CharField + .. attribute:: description: TextField + .. attribute:: members: ManyToManyField → django.contrib.auth.models.User + .. attribute:: owner: ForeignKey → django.contrib.auth.models.User + .. attribute:: access: CharField ["MANUAL", "OPEN"] + + +Estensione ai permessi di Django +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.core.permissions + +I permessi di `rest_framework` vengono estesi con due nuove classi che utilizzano il :ref:`modello di autorizzazione` precedentemente definito. + +.. class:: Edit(rest_framework.permissions.BasePermission) + + Consente l'interazione solo agli utenti che possono modificare (`.can_edit`) l'oggetto. + +.. class:: Admin(rest_framework.permissions.BasePermission) + + Consente l'interazione solo agli utenti che possono amministrare (`.can_admin`) l'oggetto. + + +Viewset astratti +^^^^^^^^^^^^^^^^ +.. module:: sophon.core.views + +Vengono definiti tre viewset in grado di utilizzare i metodi aggiunti dalle classi astratte `.models.SophonModel` e `.models.SophonGroupModel`. + +.. class:: ReadSophonViewSet(rest_framework.viewsets.ReadOnlyModelViewSet, metaclass=abc.ABCMeta) + + Classe **astratta** che estende la classe base `rest_framework.viewsets.ReadOnlyModelViewSet` con metodi di utilità mancanti nell'implementazione originale, allacciandola inoltre a `.models.SophonGroupModel`. + + .. method:: get_queryset(self) -> QuerySet + :abstractmethod: + + Imposta come astratto (e quindi obbligatorio) il metodo `rest_framework.viewsets.ReadOnlyModelViewSet.get_queryset`. + + .. method:: permission_classes(self) + :property: + + Sovrascrive il campo di classe `rest_framework.viewsets.ReadOnlyModelViewSet.permission_classes` con una funzione, permettendone la selezione dei permessi richiesti al momento di ricezione di una richiesta HTTP (invece che al momento di definizione della classe). + + Delega la selezione delle classi a `.get_permission_classes`. + + .. method:: get_permission_classes(self) -> typing.Collection[typing.Type[permissions.BasePermission]] + + Funzione che permette la selezione dei permessi necessari per effetuare una determinata richiesta al momento di ricezione di quest'ultima. + + Utile per le classi che erediteranno da questa. + + .. method:: get_serializer_class(self) -> typing.Type[Serializer] + + Funzione che permette la selezione del `rest_framework.serializers.Serializer` da utilizzare per una determinata richiesta al momento di ricezione di quest'ultima. + + Utilizza: + + - il serializzatore **in sola lettura** per elencare gli oggetti (azione ``list``); + - il serializzatore **di creazione** per creare nuovi oggetti (azione ``create``) e per generare i metadati del viewset (azione ``metadata``); + - il serializzatore ottenuto da `.models.SophonGroupModel.get_access_serializer` per la visualizzazione dettagliata (azione ``retrieve``), la modifica (azioni ``update`` e ``partial_update``) e l'eliminazione (azione ``destroy``) di un singolo oggetto; + - il serializzatore ottenuto da `.get_custom_serializer_classes` per le azioni personalizzate. + + .. seealso:: + + `.models.SophonGroupModel` + + .. method:: get_custom_serializer_classes(self) -> t.Type[Serializer] + + Permette alle classi che ereditano da questa di selezionare quale `rest_framework.serializers.Serializer` utilizzare per le azioni personalizzate. + +.. class:: WriteSophonViewSet(rest_framework.viewsets.ModelViewSet, ReadSophonViewSet, metaclass=abc.ABCMeta) + + Classe **astratta** che estende la classe base `ReadSophonViewSet` aggiungendoci i metodi di `rest_framework.viewsets.ModelViewSet` che effettuano modifiche sugli oggetti. + + Depreca i metodi ``perform_*`` di `rest_framework`, introducendone versioni migliorate con una signature diversa dal nome di ``hook_*``. + + .. method:: perform_create(self, serializer) + + .. deprecated:: 0.1 + + .. method:: perform_update(self, serializer) + + .. deprecated:: 0.1 + + .. method:: perform_destroy(self, serializer) + + .. deprecated:: 0.1 + + .. method:: hook_create(self, serializer) -> dict[str, typing.Any] + + Funzione chiamata durante l'esecuzione dell'azione di creazione oggetto ``create``. + + :param serializer: Il `~rest_framework.serializers.Serializer` già "riempito" contenente i dati dell'oggetto che sta per essere creato. + :raises .HTTPException: È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione. + :returns: Un `dict` da unire a quello del `~rest_framework.serializers.Serializer` per formare l'oggetto da creare. + + .. method:: hook_update(self, serializer) -> dict[str, t.Any] + + Funzione chiamata durante l'esecuzione delle azioni di modifica oggetto ``update`` e ``partial_update``. + + :param serializer: Il `~rest_framework.serializers.Serializer` già "riempito" contenente i dati dell'oggetto che sta per essere modificato. + :raises .HTTPException: È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione. + :returns: Un `dict` da unire a quello del `~rest_framework.serializers.Serializer` per formare l'oggetto da modificare. + + .. method:: hook_destroy(self, serializer) -> dict[str, typing.Any] + + Funzione chiamata durante l'esecuzione dell'azione di eliminazione oggetto ``destroy``. + + :raises .HTTPException: È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione. + +.. exception:: sophon.core.errors.HTTPException + + Tipo di eccezione che è possibile sollevare nei metodi ``hook_*`` di `.WriteSophonViewSet` per interrompere l'azione in corso senza applicare le modifiche. + + .. attribute:: status: int + + Permette di specificare il codice errore con cui rispondere alla richiesta interrotta. + + +.. class:: SophonGroupViewSet(WriteSophonViewSet, metaclass=abc.ABCMeta) + + Classe **astratta** che estende la classe base `.WriteSophonViewSet` estendendo gli ``hook_*`` con verifiche dei permessi dell'utente che tenta di effettuare l'azione. + + .. method:: get_group_from_serializer(self, serializer) -> models.ResearchGroup + :abstractmethod: + + Metodo necessario a trovare il gruppo a cui apparterrà un oggetto prima che il suo serializzatore venga elaborato. + + :param serializer: Il `~rest_framework.serializers.Serializer` già "riempito" contenente i dati dell'oggetto. + + +Viewset concreti +^^^^^^^^^^^^^^^^ + +Vengono poi definiti tre viewset e una view che permettono interazioni tra l'utente e i modelli definiti nell'app. + +.. class:: UsersByIdViewSet(ReadSophonViewSet) + + Viewset in sola lettura che permette di recuperare gli utenti dell'istanza partendo dal loro ``id``. + + Accessibile all'URL :samp:`/api/core/users/by-id/{ID}/`. + +.. class:: UsersByUsernameViewSet(ReadSophonViewSet) + + Viewset in sola lettura che permette di recuperare gli utenti dell'istanza partendo dal loro ``username``. + + Accessibile all'URL :samp:`/api/core/users/by-username/{USERNAME}/`. + +.. class:: ResearchGroupViewSet(WriteSophonViewSet) + + Viewset in lettura e scrittura che permette di interagire con i gruppi di ricerca. + + Accessibile all'URL :samp:`/api/core/groups/{GROUP_SLUG}/`. + + .. method:: join(self, request: Request, pk: int) -> Response + + Azione personalizzata che permette ad un utente di unirsi ad un gruppo aperto. + + Utilizza `.models.SophonGroupModel.get_access_serializer`. + + .. method:: leave(self, request: Request, pk: int) -> Response + + Azione personalizzata che permette ad un utente di abbandonare un gruppo di cui non è proprietario. + + Utilizza `.models.SophonGroupModel.get_access_serializer`. + +.. class:: SophonInstanceDetailsView(APIView) + + View che restituisce il valore attuale dell'unico oggetto `.models.SophonInstanceDetails`. + + Accessibile tramite richieste ``GET`` all'URL :samp:`/api/core/instance/`. + + +Pagina di amministrazione +^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.core.admin + +Vengono infine registrati nella pagina di amministrazione i modelli concreti definiti in questa app, effettuando alcune personalizzazioni elencate in seguito. + +.. class:: ResearchGroupAdmin(SophonAdmin) + + Per i gruppi di ricerca, viene specificato un ordinamento, permesso il filtraggio e selezionati i campi più importanti da visualizzare nella lista. + +.. class:: SophonInstanceDetails(SophonAdmin) + + Per i dettagli dell'istanza, vengono disattivate tutte le azioni, impedendo la creazione o eliminazione del singleton. + + +Testing in Sophon Core +^^^^^^^^^^^^^^^^^^^^^^ + +.. todo:: Testing in Sophon Core + + +L'app Sophon Projects +--------------------- + +.. default-domain:: py +.. default-role:: obj +.. module:: sophon.projects + +L'app `sophon.projects` è un app secondaria che dipende da `sophon.core` che introduce in Sophon il concetto di :ref:`progetto di ricerca`. + +.. caution:: + + Anche se l'app `sophon.projects` è opzionale (il progetto può funzionare senza di essa), si sconsiglia di disattivarla, in quanto il :ref:`modulo frontend` si aspetta che l'app sia attiva e solleverà un errore nel caso che i viewset forniti da questa app non siano disponibile. + + +Modello del progetto di ricerca +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.projects.models + +Viene introdotto un modello concreto che rappresenta un :ref:`progetto di ricerca`. + +.. class:: ResearchProject(SophonGroupModel) + + .. attribute:: slug: SlugField + .. attribute:: group: ForeignKey → sophon.core.models.ResearchGroup + .. attribute:: name: CharField + .. attribute:: description: TextField + .. attribute:: visibility: CharField ["PUBLIC", "INTERNAL", "PRIVATE"] + + +Viewset del gruppo di ricerca +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.projects.views + +Da una base comune, vengono creati due viewset per interagire con i progetti di ricerca. + +.. class:: ResearchProjectViewSet(SophonGroupViewSet, metaclass=abc.ABCMeta) + + Classe **astratta** che effettua l'override di `~sophon.core.views.SophonGroupView.get_group_from_serializer` per entrambi i viewset che seguono. + +.. class:: ResearchProjectsBySlugViewSet(ResearchProjectViewSet) + + Viewset in lettura e scrittura che permette di interagire con tutti i progetti di ricerca a cui l'utente loggato ha accesso. + + Accessibile all'URL :samp:`/api/projects/by-slug/{PROJECT_SLUG}/`. + +.. class:: ResearchProjectsByGroupViewSet(ResearchProjectViewSet) + + Viewset in lettura e scrittura che permette di interagire con i progetti di ricerca a cui l'utente loggato ha accesso, filtrati per il gruppo a cui appartengono. + + Il filtraggio viene effettuato limitando il queryset. + + Accessibile all'URL :samp:`/api/projects/by-group/{GROUP_SLUG}/{PROJECT_SLUG}/`. + + +Amministrazione del gruppo di ricerca +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.projects.admin + +Il modello `.models.ResearchProject` viene registrato nella pagina di amministrazione attraverso la seguente classe: + +.. class:: ResearchProjectAdmin(sophon.core.admin.SophonAdmin) + + Classe per la pagina di amministrazione che specifica un ordinamento, permette il filtraggio per gruppo di appartenenza e visibilità, e specifica i campi da visualizzare nell'elenco dei progetti. + + +L'app Sophon Notebooks +---------------------- +.. default-domain:: py +.. default-role:: obj +.. module:: sophon.notebooks + + +L'app `sophon.notebooks` è un app secondaria che dipende da `sophon.projects` che introduce in Sophon il concetto di :ref:`notebook`. + +.. caution:: + + Anche se l'app `sophon.notebooks` è opzionale (il progetto può funzionare senza di essa), si sconsiglia di disattivarla, in quanto il :ref:`modulo frontend` si aspetta che l'app sia attiva e solleverà un errore nel caso che i viewset forniti da questa app non siano disponibile. + + +Funzionamento di un notebook +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Internamente, un notebook non è altro che un container Docker accessibile ad un determinato indirizzo il cui stato è sincronizzato con un oggetto del database del :ref:`modulo backend`. + + +Modalità sviluppo +""""""""""""""""" + +Per facilitare lo sviluppo di Sophon, sono previste due modalità di operazione di quest'ultimo: + +- nella prima, la **modalità sviluppo**, il :ref:`modulo proxy` non è in esecuzione, ed è possibile collegarsi direttamente ai container attraverso collegamenti a ``localhost``; + +- nella seconda, la **modalità produzione**, il :ref:`modulo proxy` è in esecuzione all'interno di un container Docker, e si collega agli altri container attraverso i rispettivi network Docker agli indirizzi comunicatogli dal :ref:`modulo backend`. + + .. image:: notebooks_diagram.png + + +Gestione della rubrica del proxy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.notebooks.apache + +Viene creata una classe per la gestione della rubrica del proxy, utilizzando il modulo `dbm.gnu`, supportato da HTTPd. + +La rubrica mappa gli URL pubblici dei notebook a URL privati relativi al :ref:`modulo proxy`, in modo da effettuare reverse proxying **dinamico**. + +.. class:: ApacheDB + + Classe che permette il recupero, la creazione, la modifica e l'eliminazioni di chiavi di un database `dbm.gnu` come se quest'ultimo fosse un `dict` con supporto a chiavi e valori `str` e `bytes`. + + .. staticmethod:: convert_to_bytes(item: typing.Union[str, bytes]) -> bytes + + Tutte le `str` passate a questa classe vengono convertite in `bytes` attraverso questa funzione, che effettua un encoding in ASCII e solleva un errore se quest'ultimo fallisce. + + +Assegnazione porta effimera +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In *modalità sviluppo*, è necessario trovare una porta libera a cui rendere accessibile i container Docker dei notebook. + +.. function:: get_ephemeral_port() -> int + + Questa funzione apre e chiude immediatamente un `socket.socket` all'indirizzo ``localhost:0`` in modo da ricevere dal sistema operativo un numero di porta sicuramente libero. + + +Connessione al daemon Docker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.notebooks.docker + +Per facilitare l'utilizzo del daemon Docker per la gestione dei container dei notebook, viene utilizzato il modulo `docker`. + +.. function:: get_docker_client() -> docker.DockerClient + + Funzione che crea un client Docker con le variabili di ambiente del modulo. + +.. data:: client: docker.DockerClient = lazy_object_proxy.Proxy(get_docker_client) + + Viene creato un client Docker globale con inizializzazione lazy al fine di non tentare connessioni (lente!) al daemon quando non sono necessarie. + + +Controllo dello stato di salute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Il modulo `docker` viene esteso implementando supporto per l'istruzione ``HEALTHCHECK`` dei ``Dockerfile``. + +.. class:: HealthState(enum.IntEnum) + + Enumerazione che elenca gli stati possibili in cui può essere la salute di un container. + + .. attribute:: UNDEFINED = -2 + + Il ``Dockerfile`` non ha un ``HEALTHCHECK`` definito. + + .. attribute:: STARTING = -1 + + Il container Docker non mai completato con successo un ``HEALTHCHECK``. + + .. attribute:: HEALTHY = 0 + + Il container Docker ha completato con successo l'ultimo ``HEALTHCHECK`` e quindi sta funzionando correttamente. + + .. attribute:: UNHEALTHY = 1 + + Il container Docker ha fallito l'ultimo ``HEALTHCHECK``. + + +.. function:: get_health(container: docker.models.containers.Container) -> HealthState + + Funzione che utilizza l'API a basso livello del client Docker per recuperare l'`HealthState` dei container. + +.. function:: sleep_until_container_has_started(container: docker.models.containers.Container) -> HealthState + + Funzione bloccante che restituisce solo quando lo stato del container specificato non è `HealthState.STARTING`. + + +Generazione di token sicuri +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Si è scelto di rendere completamente trasparente all'utente il meccanismo di autenticazione a JupyterLab. + +Pertanto, si è verificata la necessità di generare token crittograficamente sicuri da richiedere per l'accesso a JupyterLab. + +.. function:: generate_secure_token() -> str + + Funzione che utilizza `secrets.token_urlsafe` per generare un token valido e crittograficamente sicuro. + + +Modello dei notebook +^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.notebooks.models + +Viene definito il modello rappresentante un :ref:`notebook`. + +.. class:: Notebook(SophonGroupModel) + + .. attribute:: slug: SlugField + + Lo slug dei notebook prevede ulteriori restrizioni oltre a quelle previste dallo `django.db.models.SlugField`: + + - non può essere uno dei seguenti valori: ``api``, ``static``, ``proxy``, ``backend``, ``frontend``, ``src``; + - non può iniziare o finire con un trattino ``-``. + + .. attribute:: project: ForeignKey → sophon.projects.models.ResearchProject + .. attribute:: name: CharField + .. attribute:: locked_by: ForeignKey → django.contrib.auth.models.User + + .. attribute:: container_image: CharField ["ghcr.io/steffo99/sophon-jupyter"] + + Campo che specifica l'immagine che il client Docker dovrà avviare per questo notebook. + + Al momento ne è configurata una sola per semplificare l'esperienza utente, ma altre possono essere specificate per permettere agli utenti più scelta. + + .. note:: + + Al momento, le immagini specificate devono esporre un server web sulla porta ``8888``, e supportare il protocollo di connessione di Jupyter, ovvero :samp:`{PROTOCOLLO}://immagine:8888/lab?token={TOKEN}` e :samp:`{PROTOCOLLO}://immagine:8888/tree?token={TOKEN}`. + + .. attribute:: jupyter_token: CharField + + Il token segreto che verrà passato attraverso le variabili di ambiente al container Docker dell'oggetto per permettere solo agli utenti autorizzati di accedere a quest'ultimo. + + .. attribute:: container_id: CharField + + L'id assegnato dal daemon Docker al container di questo oggetto. + + Se il notebook non è avviato, questo attributo varrà `None`. + + .. attribute:: port: IntegerField + + La porta assegnata al container Docker dell'oggetto nel caso in cui Sophon sia avviato in "modalità sviluppo", ovvero con il :ref:`modulo proxy` in esecuzione sul sistema host. + + .. attribute:: internal_url: CharField + + L'URL a cui è accessibile il container Docker dell'oggetto nel caso in cui Sophon non sia avviato in "modalità sviluppo", ovvero con il :ref:`modulo proxy` in esecuzione all'interno di un container. + + .. method:: log(self) -> logging.Logger + :property: + + Viene creato un `logging.Logger` per ogni oggetto della classe, in modo da facilitare il debug relativo ad uno specifico notebook. + + Il nome del logger ha la forma :samp:`sophon.notebooks.models.Notebook.{NOTEBOOK_SLUG}`. + + .. method:: enable_proxying(self) -> None + + Aggiunge l'indirizzo del notebook alla rubrica del proxy. + + .. method:: disable_proxying(self) -> None + + Rimuove l'indirizzo del notebook dalla rubrica del proxy. + + .. method:: sync_container(self) -> t.Optional[docker.models.containers.Container] + + Sincronizza lo stato dell'oggetto nel database con lo stato del container Docker nel sistema. + + .. method:: create_container(self) -> docker.models.containers.Container + + Crea e configura un container Docker per l'oggetto, con l'immagine specificata in `.container_image`. + + .. method:: start(self) -> None + + Tenta di creare e avviare un container Docker per l'oggetto, bloccando fino a quando esso non sarà avviato con `~.docker.sleep_until_container_has_started`. + + .. method:: stop(self) -> None + + Arresta il container Docker dell'oggetto. + + +Viewset dei notebook +^^^^^^^^^^^^^^^^^^^^ +.. module:: sophon.notebooks.views + +Come per il modulo `sophon.projects`, vengono creati due viewset per interagire con i progetti di ricerca, basati entrambi su un viewset astratto che ne definisce le proprietà comuni. + +.. class:: NotebooksViewSet(SophonGroupViewSet, metaclass=abc.ABCMeta) + + Classe **astratta** che effettua l'override di `~sophon.core.views.SophonGroupView.get_group_from_serializer` e definisce cinque azioni personalizzate per l'interazione con il notebook. + + .. method:: sync(self, request: Request, **kwargs) -> Response + + Azione personalizzata che sincronizza lo stato dell'oggetto dell'API con quello del daemon Docker. + + .. method:: start(self, request: Request, **kwargs) -> Response + + Azione personalizzata che avvia il notebook con `.models.Notebook.start`. + + .. method:: stop(self, request: Request, **kwargs) -> Response + + Azione personalizzata che arresta il notebook con `.models.Notebook.stop`. + + .. method:: lock(self, request: Request, **kwargs) -> Response + + Azione personalizzata che blocca il notebook impostando il campo `.models.Notebook.locked_by` all'utente che ha effettuato la richiesta. + + .. method:: unlock(self, request: Request, **kwargs) -> Response + + Azione personalizzata che sblocca il notebook impostando il campo `.models.Notebook.locked_by` a `None`. + +.. class:: NotebooksBySlugViewSet(NotebooksViewSet) + + Viewset in lettura e scrittura che permette di interagire con tutti i notebook a cui l'utente loggato ha accesso. + + Accessibile all'URL :samp:`/api/notebooks/by-slug/{NOTEBOOK_SLUG}/`. + +.. class:: NotebooksByProjectViewSet(NotebooksViewSet) + + Viewset in lettura e scrittura che permette di interagire con i notebook a cui l'utente loggato ha accesso, filtrati per il progetto di appartenenza. + + Accessibile all'URL :samp:`/api/notebooks/by-project/{PROJECT_SLUG}/{NOTEBOOK_SLUG}/`. + + +Dockerizzazione del modulo backend +---------------------------------- + +.. todo:: Dockerizzazione + +Modulo frontend +=============== + +.. todo:: Modulo frontend + + +Struttura delle directory +------------------------- +.. default-domain:: js + +Le directory di :mod:`@steffo45/sophon-frontend` sono strutturate nella seguente maniera: + +src/components + Contiene i componenti React sia con le classi sia funzionali. + +src/contexts + Contiene i contesti React creati con :func:`React.createContext`. + +src/hooks + Contiene gli hook React personalizzati utilizzati nei componenti funzionali. + +src/types + Contiene estensioni ai tipi base TypeScript, come ad esempio i tipi restituiti dalla web API del :ref:`modulo backend`. + +src/utils + Contiene varie funzioni di utility. + +public + Contiene i file statici da servire assieme all'app. + + +Comunicazione con il server +--------------------------- +.. default-domain:: js + + +Axios +^^^^^ + +Per effettuare richieste all'API web, si è deciso di utilizzare la libreria :mod:`axios`, in quanto permette di creare dei "client" personalizzabili con varie proprietà. + +In particolare, si è scelto di forkarla, integrando anticipatamente una proposta di funzionalità che permette alle richieste di essere interrotte attraverso degli :class:`AbortController`. + + +Client personalizzati +^^^^^^^^^^^^^^^^^^^^^ + +Per permettere all'utente di selezionare l'istanza da utilizzare e di comunicare con l'API con le proprie credenziali, si è scelto di creare client personalizzati partendo da due contesti. + +All'interno di un contesto in cui è stata selezionata un'istanza (:data:`InstanceContext`), viene creato un client dal seguente hook: + +.. function:: useInstanceAxios(config = {}) + + Questo hook specifica il ``baseURL`` del client Axios, impostandolo all'URL dell'istanza selezionata. + +All'interno di un contesto in cui è stato effettuato l'accesso come utente (:data:`AuthorizationContext`), viene creato invece un client dal seguente hook: + +.. function:: useAuthorizedAxios(config = {}) + + Questo hook specifica il valore dell'header ``Authorization`` da inviare in tutte le richieste effettuate a :samp:`Bearer {TOKEN}`, utilizzando il token ottenuto al momento dell'accesso. + + +Utilizzo di viewset +^^^^^^^^^^^^^^^^^^^ + +Viene implementato un hook che si integra con i viewset di Django, fornendo un API semplificato per effettuare azioni su di essi. + +.. function:: useViewSet(baseRoute) + + Questo hook implementa tutte le azioni :py:mod:`rest_framework` di un viewset in lettura e scrittura. + + Richiede di essere chiamato all'interno di un :data:`AuthorizationContext`. + + .. function:: async list(config = {}) + .. function:: async retrieve(pk, config = {}) + .. function:: async create(config) + .. function:: async update(pk, config) + .. function:: async destroy(pk, config) + + Viene inoltre fornito supporto per le azioni personalizzate. + + .. function:: async command(config) + + Permette azioni personalizzate su tutto il viewset. + + .. function:: async action(config) + + Permette azioni personalizzate su uno specifico oggetto del viewset. + + +Emulazione di viewset +^^^^^^^^^^^^^^^^^^^^^ + +Viene creato un hook che tiene traccia degli oggetti restituiti da un determinato viewset, ed emula i risultati delle azioni effettuate, minimizzando i rerender e ottenendo una ottima user experience. + +.. function:: useManagedViewSet(baseRoute, pkKey, refreshOnMount) + + .. attribute:: viewset + + Il viewset restituito da :func:`useViewSet`, utilizzato come interfaccia di basso livello per effettuare azioni. + + .. attribute:: state + + Lo stato del viewset, che tiene traccia degli oggetti e delle azioni in corso su di essi. + + Gli oggetti all'interno di esso sono istanze di :class:`ManagedResource`, create usando wrapper di :func:`.update`, :func:`.destroy` e :func:`.action`, che permettono di modificare direttamente l'oggetto senza preoccuparsi dell'indice a cui si trova nell'array. + + .. attribute:: dispatch + + Riduttore che permette di alterare lo :attr:`.state`. + + .. function:: async refresh() + + Ricarica gli oggetti del viewset. + + Viene chiamata automaticamente al primo render se ``refreshOnMount`` è :data:`True`. + + .. function:: async create(data) + + Crea un nuovo oggetto nel viewset con i dati specificati come argomento, e lo aggiunge allo stato se la richiesta va a buon fine. + + .. function:: async command(method, cmd, data) + + Esegue l'azione personalizzata ``cmd`` su tutto il viewset, utilizzando il metodo ``method`` e con i dati specificati in ``data``. + + Se la richiesta va a buon fine, il valore restituito dal backend sostituisce nello stato le risorse dell'intero viewset. + + .. function:: async update(index, data) + + Modifica l'oggetto alla posizione ``index`` dell'array :attr:`.state` con i dati specificati in ``data``. + + Se la richiesta va a buon fine, la modifica viene anche applicata all'interno di :attr:`.state` + + .. function:: async destroy(index) + + Elimina l'oggetto alla posizione ``index`` dell'array :attr:`.state`. + + Se la richiesta va a buon fine, l'oggetto viene eliminato anche da :attr:`.state`. + + .. function:: async action(index, method, act, data) + + Esegue l'azione personalizzata ``act`` sull'oggetto alla posizione ``index`` dell'array :attr:`.state`, utilizzando il metodo ``method`` e con i dati specificati in ``data``. + + Se la richiesta va a buon fine, il valore restituito dal backend sostituisce l'oggetto utilizzato in :attr:`.state`. + + +Contesti innestati +------------------ +.. default-domain:: js + +Per minimizzare i rerender, l'applicazione è organizzata a "contesti innestati". + + +I contesti +^^^^^^^^^^ + +Viene definito un contesto per ogni tipo di risorsa selezionabile nell'interfaccia. + +Essi sono, in ordine dal più esterno al più interno: + +#. :data:`InstanceContext` (:ref:`Istanza`) +#. :data:`AuthorizationContext` (:ref:`Utente`) +#. :data:`GroupContext` (:ref:`Gruppo di ricerca`) +#. :data:`ProjectContext` (:ref:`Progetto di ricerca`) +#. :data:`NotebookContext` (:ref:`Notebook`) + + +Contenuto dei contesti +"""""""""""""""""""""" + +Questi contesti possono avere tre tipi di valori: :data:`undefined` se ci si trova al di fuori del contesto, :data:`null` se non è stato selezionato alcun oggetto oppure **l'oggetto selezionato** se esso esiste. + + +URL contestuale +^^^^^^^^^^^^^^^ + +Si è definita la seguente struttura per gli URL del frontend di Sophon, in modo che essi identificassero universalmente una risorsa e che essi fossero human-readable. + +.. code-block:: text + + /i/{ISTANZA} + /l/logged-in + /g/{GROUP_SLUG} + /p/{PROJECT_SLUG} + /n/{NOTEBOOK_SLUG}/ + +Ad esempio, l'URL per il notebook ``my-first-notebook`` dell'istanza demo di Sophon sarebbe: + +.. code-block:: text + + /i/https:api.prod.sophon.steffo.eu: + /l/logged-in + /g/my-first-group + /p/my-first-project + /n/my-first-notebook/ + + +Parsing degli URL contestuali +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Viene definita una funzione in grado di comprendere gli URL contestuali: + +.. function:: parsePath(path) + + :param path: Il "path" da leggere. + :returns: + Un oggetto con le seguenti chiavi, dette "segmenti di percorso", le quali possono essere :data:`undefined` per indicare che non è stato selezionato un oggetto di quel tipo: + + - ``instance``: l'URL dell'istanza da utilizzare, con caratteri speciali sostituiti da ``:`` + - ``loggedIn``: :class:`Boolean`, se :data:`True` l'utente ha effettuato il login (come :ref:`Ospite` o :ref:`Utente`) + - ``researchGroup``: lo slug del :ref:`gruppo di ricerca` selezionato + - ``researchProject``: lo slug del :ref:`progetto di ricerca` selezionato + - ``notebook``: lo slug del :ref:`notebook` selezionato + + Ad esempio, l'URL precedente restituirebbe il seguente oggetto se processato: + + .. code-block:: js + + { + "instance": "https:api.prod.sophon.steffo.eu:", + "loggedIn": True, + "researchGroup": "my-first-group", + "researchProject": "my-first-project", + "notebook": "my-first-notebook" + } + + +Routing basato sui contesti +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +I valori dei contesti vengono utilizzati per selezionare i componenti da mostrare all'utente nell'interfaccia grafica attraverso i seguenti componenti: + +.. function:: ResourceRouter({selection, unselectedRoute, selectedRoute}) + + Componente che sceglie se renderizzare ``unselectedRoute`` o ``selectedRoute`` in base alla *nullità* o *non-nullità* di ``selection``. + +.. function:: ViewSetRouter({viewSet, unselectedRoute, selectedRoute, pathSegment, pkKey}) + + Componente basato su :func:`ResourceRouter` che seleziona automaticamente l'elemento del viewset avente il valore del segmento di percorso ``pathSegment`` alla chiave ``pkKey``. + + +Esempio di utilizzo di ViewSetRouter +"""""""""""""""""""""""""""""""""""" + +.. function:: GroupRouter({...props}) + + Implementato come: + + .. code-block:: tsx + + ("/api/core/groups/", "slug")} + pathSegment={"researchGroup"} + pkKey={"slug"} + /> + + +Albero completo dei contesti +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +L'insieme di tutti i contesti è definito come componente :func:`App` nel modulo "principale" ``App.tsx``. + +Se ne riassume la struttura in pseudocodice: + +.. code-block:: html + + + + unselected: + + selected: + + + unselected: + + selected: + + + unselected: + + selected: + + + unselected: + + selected: + + + unselected: + + selected: + + + +Altri contesti +^^^^^^^^^^^^^^ + +Tema +"""" + +Il tema dell'istanza è implementato come uno speciale contesto globale :data:`ThemeContext` che riceve i dettagli dell'istanza a cui si è collegati dall':data:`InstanceContext`. + + +Cache +""""" + +Viene salvato l'elenco di tutti i membri dell':ref:`istanza` in uno speciale contesto :data:`CacheContext` in modo da poter risolvere gli id degli utenti al loro username senza dover effettuare ulteriori richieste. + + +Dockerizzazione del modulo frontend +----------------------------------- + +.. todo:: Dockerizzazione del modulo frontend + + +Modulo proxy +============ + +Il *modulo proxy* consiste in un webserver che riceve tutte le richieste HTTP dirette ad uno degli altri moduli e le smista in base a regole statiche e dinamiche. + +È collocato all'interno del repository in ``/proxy``. + + +Tecnologie utilizzate +--------------------- + +- Il server web `Apache HTTPd`_ + - Il modulo `rewrite`_ + - Il modulo `proxy`_ + - Il modulo `proxy_http`_ + - Il modulo `proxy_wstunnel`_ + +.. _Apache HTTPd: https://httpd.apache.org/ +.. _rewrite: https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html +.. _proxy: https://httpd.apache.org/docs/2.4/mod/mod_proxy.html +.. _proxy_http: https://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html +.. _proxy_wstunnel: https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html + + +Funzionamento del modulo +------------------------ + +Il modulo proxy è composto da un file di configurazione di `Apache HTTPd`_ e di un ``Dockerfile`` che lo copia all'interno dell'immagine Docker ufficiale ``httpd:2.4``. + +Il file di configurazione abilita i moduli `rewrite`_, `proxy`_, `proxy_wstunnel`_ e `proxy_http`_, impostando quest'ultimo per inoltrare l'header `Host `_ alle pagine verso cui viene effettuato reverse proxying. + +Inoltre, nel file di configurazione viene abilitato il ``RewriteEngine``, che viene utilizzato per effettuare reverse proxying secondo le seguenti regole: + +#. Tutte le richieste verso ``static.`` prefisso ad :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` vengono processate direttamente dal webserver, utilizzando i file disponibili nella cartella ``/var/www/html/django-static`` che gli vengono forniti dal volume ``django-static`` del :ref:`modulo backend`. + + .. code-block:: apacheconf + + # If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST + RewriteCond "static.%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] + # Process the request yourself + RewriteRule ".?" - [L] + +#. Tutte le richieste verso :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` senza nessun sottodominio vengono inoltrate al container Docker del :ref:`modulo frontend` utilizzando la risoluzione dei nomi di dominio di Docker Compose. + + .. code-block:: apacheconf + + # If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST + RewriteCond "%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] + # Capture ENV:SOPHON_FRONTEND_NAME for substitution in the rewriterule + RewriteCond "%{ENV:SOPHON_FRONTEND_NAME}" "^(.+)$" [NC] + # Forward to the frontend + RewriteRule "/(.*)" "http://%1/$1" [P,L] + +#. Tutte le richieste verso ``api.`` prefisso ad :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` vengono inoltrate al container Docker del :ref:`modulo backend` utilizzando la risoluzione dei nomi di dominio di Docker Compose. + + .. code-block:: apacheconf + + # If api. prefixed to ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST + RewriteCond "api.%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] + # Capture ENV:SOPHON_BACKEND_NAME for substitution in the rewriterule + RewriteCond "%{ENV:SOPHON_BACKEND_NAME}" "^(.+)$" [NC] + # Forward to the backend + RewriteRule "/(.*)" "http://%1/$1" [P,L] + +#. Carica in memoria la rubrica dei notebook generata dal :ref:`modulo backend` e disponibile in ``/run/sophon/proxy/proxy.dbm`` attraverso il volume ``proxy-data``, assegnandogli il nome di ``sophonproxy``. + + .. code-block:: apacheconf + + # Create a map between the proxy file generated by Sophon and Apache + RewriteMap "sophonproxy" "dbm=gdbm:/run/sophon/proxy/proxy.dbm" + +#. Effettua il proxying dei websocket verso i notebook mappati dalla rubrica ``sophonproxy``. + + .. code-block:: apacheconf + + # If this is any other subdomain of ENV:APACHE_PROXY_BASE_DOMAIN + RewriteCond ".%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) [^ ]+\1$" [NC] + # If this is a websocket connection + RewriteCond "%{HTTP:Connection}" "Upgrade" [NC] + RewriteCond "%{HTTP:Upgrade}" "websocket" [NC] + # Forward to the notebook + RewriteRule "/(.*)" "ws://${sophonproxy:%{HTTP_HOST}}/$1" [P,L] + +#. Effettua il proxying delle richieste "normali" verso i notebook mappati dalla rubrica ``sophonproxy``. + + .. code-block:: apacheconf + + # If this is any other subdomain of ENV:APACHE_PROXY_BASE_DOMAIN + RewriteCond ".%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) [^ ]+\1$" [NC] + # Forward to the notebook + RewriteRule "/(.*)" "http://${sophonproxy:%{HTTP_HOST}}/$1" [P,L] + +Tutte le regole usano il flag ``L`` di ``RewriteRule``, che porta il motore di rewriting a ignorare tutte le regole successive, come il ``return`` di una funzione di un linguaggio di programmazione imperativo. + +.. note:: + + I blocchi di codice all'interno di questa sezione sono stati inseriti manualmente e potrebbero non essere interamente aggiornati alla versione più recente del file. + + Si consiglia di consultare il file ``httpd.conf`` in caso si necessiti di informazioni aggiornate. + + +Dockerizzazione del modulo proxy +-------------------------------- + +.. todo:: Dockerizzazione del modulo proxy + + +Modulo Jupyter +============== + +Il *modulo Jupyter* consiste in un ambiente `Jupyter `_ e `JupyterLab `_ modificato per una migliore integrazione con Sophon, in particolare con il :ref:`modulo frontend` e il :ref:`modulo backend`. + +È collocato all'interno del repository in ``/jupyter``. + +Progetti utilizzati +------------------- + +- Le immagini Docker ufficiali di Jupyter `jupyter/docker-stacks `_ +- Il tema `JupyterLab Sophon `_ (realizzato durante il tirocinio) +- Il tool per il trasferimento dati `curl `_ + + +Sviluppo del tema per Jupyter +============================= + +.. todo:: Tema personalizzato di Jupyter + + +Funzionamento del modulo +------------------------ + +Il modulo è composto da un singolo ``Dockerfile`` che crea un immagine Docker in quattro fasi: + +#. **Base**: Parte dall'immagine base ``jupyter/scipy-notebook`` ed altera i label dell'immagine; + + .. code-block:: docker + + FROM jupyter/scipy-notebook AS base + # Set the maintainer label + LABEL maintainer="Stefano Pigozzi " + +#. **Env**: Configura le variabili di ambiente dell'immagine, attivando JupyterLab, configurando il riavvio automatico di Jupyter e permettendo all'utente non-privilegiato di acquisire i privilegi di root attraverso il comando ``sudo``; + + .. code-block:: docker + + FROM base AS env + # Set useful envvars for Sophon notebooks + ENV JUPYTER_ENABLE_LAB=yes + ENV RESTARTABLE=yes + ENV GRANT_SUDO=yes + +#. **Extensions**: Installa, abilita e configura le estensioni necessarie all'integrazione con Sophon (attualmente, soltanto il tema JupyterLab Sophon); + + .. code-block:: docker + + FROM env AS extensions + # As the default user... + USER ${NB_UID} + WORKDIR "${HOME}" + # Install the JupyterLab Sophon theme + RUN jupyter labextension install "jupyterlab_theme_sophon" + # Enable the JupyterLab Sophon theme + RUN jupyter labextension enable "jupyterlab_theme_sophon" + # Set the JupyterLab Sophon theme as default + RUN mkdir -p '.jupyter/lab/user-settings/@jupyterlab/apputils-extension/' + RUN echo '{"theme": "JupyterLab Sophon"}' > ".jupyter/lab/user-settings/@jupyterlab/apputils-extension/themes.jupyterlab-settings" + +#. **Healthcheck**: Installa ``curl``, e aggiunge all'immagine un controllo per verificarne lo stato di avvio, permettendo al :ref:`modulo backend` di comunicare una richiesta di avvio riuscita solo quando l'intera immagine è avviata + + .. code-block:: docker + + FROM extensions AS healthcheck + # As root... + USER root + # Install curl + RUN apt-get update + RUN apt-get install -y curl + # Use curl to check the health status + HEALTHCHECK --start-period=5s --timeout=5s --interval=10s CMD ["curl", "--output", "/dev/null", "http://localhost:8888"] + + # We probably should go back to the default user + USER ${NB_UID} + +.. note:: + + I blocchi di codice all'interno di questa sezione sono stati inseriti manualmente e potrebbero non essere interamente aggiornati alla versione più recente del file. + + Si consiglia di consultare il ``Dockerfile`` in caso si necessiti di informazioni aggiornate. + + +Strumenti di sviluppo +===================== + +.. todo:: Strumenti di sviluppo + + +Continuous Integration +---------------------- + +.. todo:: Continuous Integration + + +Continuous Deployment +--------------------- + +.. todo:: Continuous Deployment