From 01a4225a84970c48c426dd1d2c36d5091adf6868 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Am=C3=A9lie=20Krej=C4=8D=C3=AD?= <krejcar25@blep.cz>
Date: Mon, 11 Sep 2023 21:16:51 +0200
Subject: [PATCH 01/17] Add django-auth-ldap dependency

---
 conf/requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/conf/requirements.txt b/conf/requirements.txt
index 64206b8..a9390be 100644
--- a/conf/requirements.txt
+++ b/conf/requirements.txt
@@ -20,3 +20,4 @@ djangorestframework==3.14.0
 drf-nested-routers==0.93.4
 drf-yasg==1.21.7
 markdown>=3.4.1
+django-auth-ldap==4.5.0

From 74ee2c073a05995d454a5c35a753d92f43a179f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Am=C3=A9lie=20Krej=C4=8D=C3=AD?= <krejcar25@blep.cz>
Date: Mon, 11 Sep 2023 21:19:36 +0200
Subject: [PATCH 02/17] Remove old LDAP backend files

---
 webvirtcloud/.dec_ldap_pwd.sh |  18 -----
 webvirtcloud/ldapbackend.py   | 142 ----------------------------------
 2 files changed, 160 deletions(-)
 delete mode 100755 webvirtcloud/.dec_ldap_pwd.sh
 delete mode 100644 webvirtcloud/ldapbackend.py

diff --git a/webvirtcloud/.dec_ldap_pwd.sh b/webvirtcloud/.dec_ldap_pwd.sh
deleted file mode 100755
index 2cda920..0000000
--- a/webvirtcloud/.dec_ldap_pwd.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash 
-
-##### 
-
-# 
-
-# LDAP PASSWORD DECRYPTION SCRIPT
-
-# 
-
-# 
-
-##### 
-
-ENC_PASSWD=$1 
-
-echo $(echo $ENC_PASSWD | base64 -d | openssl enc -pbkdf2 -salt -d -pass pass:MYPASSPHRASE )
-
diff --git a/webvirtcloud/ldapbackend.py b/webvirtcloud/ldapbackend.py
deleted file mode 100644
index 0f7a797..0000000
--- a/webvirtcloud/ldapbackend.py
+++ /dev/null
@@ -1,142 +0,0 @@
-from django.contrib.auth.backends import ModelBackend
-from django.contrib.auth.models import User, Group
-from django.conf import settings
-from accounts.models import UserAttributes, UserInstance, UserSSHKey
-from django.contrib.auth.models import Permission
-from logs.models import Logs
-import uuid
-
-try:
-    from ldap3 import Server, Connection, ALL
-    #/srv/webvirtcloud/ldap/ldapbackend.py
-    class LdapAuthenticationBackend(ModelBackend):
-         
-        def get_LDAP_user(self, username, password, filterString):
-            print('get_LDAP_user {}'.format(username))
-            try:
-                server = Server(settings.LDAP_URL, port=settings.LDAP_PORT,
-                    use_ssl=settings.USE_SSL,get_info=ALL)
-                connection = Connection(server,
-                                        settings.LDAP_MASTER_DN,
-                                        settings.LDAP_MASTER_PW, auto_bind=True)
-                connection.search(settings.LDAP_ROOT_DN, 
-                    '(&({attr}={login})({filter}))'.format(
-                        attr=settings.LDAP_USER_UID_PREFIX, 
-                        login=username,
-                        filter=filterString), attributes=['*'])
-
-                if len(connection.response) == 0:
-                    print('get_LDAP_user-no response')
-                    return None
-                specificUser = connection.response[0]
-                userDn = str(specificUser.get('raw_dn'),'utf-8')
-                userGivenName = connection.entries[0].givenName
-                userSn = connection.entries[0].sn
-                userMail = connection.entries[0].mail
-                with Connection(server, userDn, password) as con:
-                    return username, userGivenName, userSn, userMail
-            except Exception as e:
-                print("LDAP Exception: {}".format(e))
-                return None
-            return None
-    
-        def authenticate(self, request, username=None, password=None, **kwargs):
-            if not settings.LDAP_ENABLED:
-                return None
-            print("authenticate_ldap")
-            # Get the user information from the LDAP if he can be authenticated
-            isAdmin = False
-            isStaff = False
-            isTechnician = False
-
-            requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_ADMINS)
-            isAdmin = requeteLdap is not None
-            isStaff = requeteLdap is not None
-
-            if requeteLdap is None:
-                requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_STAFF)
-                isStaff = requeteLdap is not None
-
-            if requeteLdap is None:
-                requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_TECHNICIANS)
-                isTechnician = requeteLdap is not None
-
-            if requeteLdap is None:
-                requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_USERS)
-
-            if requeteLdap is None:
-                print("User does not belong to any search group. Check LDAP_SEARCH_GROUP_FILTER in settings.")
-                return None
-
-            techniciansGroup = Group.objects.get(name='Technicians')
-
-            try:
-                user = User.objects.get(username=username)
-                attributes = UserAttributes.objects.get(user=user)
-                user.is_staff = isStaff
-                user.is_superuser = isAdmin
-                if not isTechnician and user.groups.filter(name='Technicians').exists():
-                    user.groups.remove(techniciansGroup)
-                elif isTechnician and not user.groups.filter(name='Technicians').exists():
-                    user.groups.add(techniciansGroup)
-                else:
-                    print("The user is already in the Technicians group")
-                    user.save()
-                # TODO VERIFY
-            except User.DoesNotExist:
-                print(f"authenticate-create new user: {username}")
-                user = User(username=username)
-                user.first_name = requeteLdap[1]
-                user.last_name = requeteLdap[2]
-                user.email = requeteLdap[3]
-                user.is_active = True
-                user.is_staff = isStaff
-                user.is_superuser = isAdmin
-                user.set_password(uuid.uuid4().hex)
-                user.save()
-                if isTechnician:
-                    user.groups.add(techniciansGroup)
-                maxInstances = 1
-                maxCpus = 1
-                maxMemory = 128
-                maxDiskSize = 1
-                if isStaff:
-                    maxMemory = 2048
-                    maxDiskSize = 20
-                    permission = Permission.objects.get(codename='clone_instances')
-                    user.user_permissions.add(permission)
-                if isAdmin:
-                    maxInstances = -1
-                    maxCpus = -1
-                    maxMemory = -1
-                    maxDiskSize = -1
-                    permission = Permission.objects.get(codename='clone_instances')
-                    user.user_permissions.add(permission)
-                user.save()
-                UserAttributes.objects.create(
-                     user=user,
-                     max_instances=maxInstances,
-                     max_cpus=maxCpus,
-                     max_memory=maxMemory,
-                     max_disk_size=maxDiskSize,
-                )            
-                user.save()
-                
-                print("authenticate-user created")
-            return user
-    
-        def get_user(self, user_id):
-            if not settings.LDAP_ENABLED:
-                 return None
-            print("get_user_ldap")
-            try:
-                return User.objects.get(pk=user_id)
-            except User.DoesNotExist:
-                print("get_user-user not found")
-                return None
-except:
-    class LdapAuthenticationBackend(ModelBackend):
-        def authenticate(self, request, username=None, password=None, **kwargs):
-            return None
-        def get_user(self, user_id):
-            return None

From 561fedfccd4ec8a04174af644ab6c29a605a9bbf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Am=C3=A9lie=20Krej=C4=8D=C3=AD?= <krejcar25@blep.cz>
Date: Mon, 11 Sep 2023 21:39:05 +0200
Subject: [PATCH 03/17] Add new template config and README blocks

---
 README.md                         | 82 +++++++++++++++++++------------
 webvirtcloud/settings.py.template | 48 +++++++++---------
 2 files changed, 73 insertions(+), 57 deletions(-)

diff --git a/README.md b/README.md
index cbbfb29..7f9069f 100644
--- a/README.md
+++ b/README.md
@@ -385,55 +385,73 @@ python manage.py test
 
 ## LDAP Configuration
 
-The example settings are based on an OpenLDAP server with groups defined as "cn" of class "groupOfUniqueNames"
+The config options below can be changed in `webvirtcloud/settings.py` file. Variants for Active Directory and OpenLDAP are shown. This is a minimal config to get LDAP running, for further info read the [django-auth-ldap documentation](https://django-auth-ldap.readthedocs.io).
 
 Enable LDAP
 
 ```bash
-sudo sed -i "s/LDAP_ENABLED = False/LDAP_ENABLED = True/g"" /srv/webvirtcloud/webvirtcloud/settings.py
+sudo sed -i "s~#\"django_auth_ldap.backend.LDAPBackend\",~\"django_auth_ldap.backend.LDAPBackend\",~g" /srv/webvirtcloud/webvirtcloud/settings.py
 ```
 
-Set the LDAP server name and root DN
+Set the LDAP server name and bind DN
 
-```bash
-sudo sed -i "s/LDAP_URL = ''/LDAP_URL = 'myldap.server.com'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-sudo sed -i "s/LDAP_ROOT_DN = ''/LDAP_ROOT_DN = 'dc=server,dc=com'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
+```python
+# Active Directory
+AUTH_LDAP_SERVER_URI = "ldap://example.com"
+AUTH_LDAP_BIND_DN = "username@example.com"
+AUTH_LDAP_BIND_PASSWORD = "password"
+
+# OpenLDAP
+AUTH_LDAP_SERVER_URI = "ldap://example.com"
+AUTH_LDAP_BIND_DN = "CN=username,CN=Users,OU=example,OU=com"
+AUTH_LDAP_BIND_PASSWORD = "password"
 ```
 
-Set the passphrase to decrypt the password
-```bash
-sudo sed -i "s/pass:MYPASSPHRASE/pass:MYTRUEPASSPHRASE/g" /srv/webvirtcloud/webvirtcloud/.dec_ldap_pwd.sh
+Set the user filter and user and group search base and filter
+
+```python
+# Active Directory
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"
+)
+AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=group)"
+)
+AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()
+
+# OpenLDAP
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(cn=%(user)s)"
+)
+AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfUniqueNames)"
+)
+AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()  # import needs to be changed at the top of settings.py
 ```
 
-Encrypt the password
-```bash
-echo MYPASSWORD | openssl enc -pbkdf2 -salt -pass pass:MYTRUEPASSPHRASE | base64
+Set group which is required to access WebVirtCloud. You may set this to `False` to disable this filter.
+
+```python
+AUTH_LDAP_REQUIRE_GROUP = "CN=WebVirtCloud Access,CN=Users,DC=example,DC=com"
 ```
 
-Set the user that has browse access to LDAP and its password encrypted
+Populate user fields with values from LDAP
 
-```bash
-sudo sed -i "s/LDAP_MASTER_DN = ''/LDAP_MASTER_DN = 'cn=admin,ou=users,dc=kendar,dc=org'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-sudo sed -i "s/LDAP_MASTER_PW_ENC = ''/LDAP_MASTER_PW_ENC = 'MYPASSWORDENCRYPTED'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
+```python
+AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+    "is_staff": "CN=WebVirtCloud Staff,CN=Users,DC=example,DC=com",
+    "is_superuser": "CN=WebVirtCloud Admins,CN=Users,DC=example,DC=com",
+}
+AUTH_LDAP_USER_ATTR_MAP = {
+    "first_name": "givenName",
+    "last_name": "sn",
+    "email": "mail",
+}
 ```
 
-Set the attribute that will be used to find the username, i usually use the cn
+Now when you login with an LDAP user it will be assigned the rights defined. The user will be authenticated then with LDAP and authorized through the WebVirtCloud permissions.
 
-```bash
-sudo sed -i "s/LDAP_USER_UID_PREFIX = ''/LDAP_USER_UID_PREFIX = 'cn'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-```
-
-You can now create the filters to retrieve the users for the various group. This will be used during the user creation only
-
-```bash
-sudo sed -i "s/LDAP_SEARCH_GROUP_FILTER_ADMINS = ''/LDAP_SEARCH_GROUP_FILTER_ADMINS = 'memberOf=cn=admins,dc=kendar,dc=org'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-sudo sed -i "s/LDAP_SEARCH_GROUP_FILTER_STAFF = ''/LDAP_SEARCH_GROUP_FILTER_STAFF = 'memberOf=cn=staff,dc=kendar,dc=org'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-sudo sed -i "s/LDAP_SEARCH_GROUP_FILTER_USERS = ''/LDAP_SEARCH_GROUP_FILTER_USERS = 'memberOf=cn=users,dc=kendar,dc=org'/g"" /srv/webvirtcloud/webvirtcloud/settings.py
-```
-
-Now when you login with an LDAP user it will be assigned the rights defined. The user will be authenticated then with ldap and authorized through the WebVirtCloud permissions.
-
-If you'd like to move a user from ldap to WebVirtCloud, just change its password from the UI and (eventually) remove from the group in ldap
+If you'd like to move a user from ldap to WebVirtCloud, just change its password from the UI and (eventually) remove from the group in LDAP.
 
 
 ## REST API / BETA
diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index 50dce23..fabe94c 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -3,7 +3,9 @@ Django settings for webvirtcloud project.
 
 """
 
+import ldap
 import subprocess
+from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType
 from pathlib import Path
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -101,7 +103,7 @@ DATABASES = {
 
 AUTHENTICATION_BACKENDS = [
     "django.contrib.auth.backends.ModelBackend",
-    "webvirtcloud.ldapbackend.LdapAuthenticationBackend",
+    #"django_auth_ldap.backend.LDAPBackend",
 ]
 
 LOGIN_URL = "/accounts/login/"
@@ -280,27 +282,23 @@ EMAIL_HOST_PASSWORD = ''
 # LDAP Config
 #
 
-LDAP_ENABLED = False
-LDAP_URL = ''
-LDAP_PORT = 389
-USE_SSL = False
-## The user with search rights on ldap. (e.g cn=admin,dc=kendar,dc=org)
-LDAP_MASTER_DN = ''
-LDAP_MASTER_PW_ENC = ''
-LDAP_MASTER_PW = subprocess.Popen(["bash", str(BASE_DIR) + "/webvirtcloud/.dec_ldap_pwd.sh", LDAP_MASTER_PW_ENC],stdout=subprocess.PIPE, encoding='utf8').stdout.read().strip('\n')
-## The root dn (e.g. dc=kendar,dc=org)
-LDAP_ROOT_DN = ''
-## Queries to identify the users, i use groupOfUniqueNames on openldap
-
-### PLEASE BE SURE memberOf overlay is activated on slapd
-## e.g. memberOf=cn=admins,cn=staff,cn=technicians,cn=webvirtcloud,ou=groups,dc=kendar,dc=org
-LDAP_SEARCH_GROUP_FILTER_ADMINS = ''
-## e.g. memberOf=cn=staff,cn=technicians,cn=webvirtcloud,ou=groups,dc=kendar,dc=org
-LDAP_SEARCH_GROUP_FILTER_STAFF = ''
-## e.g. memberOf=cn=technicians,cn=webvirtcloud,ou=groups,dc=kendar,dc=org
-LDAP_SEARCH_GROUP_FILTER_TECHNICIANS = ''
-## e.g. memberOf=cn=webvirtcloud,ou=groups,dc=kendar,dc=org
-LDAP_SEARCH_GROUP_FILTER_USERS = ''
-
-## The user name prefix to identify the user name (e.g. cn)
-LDAP_USER_UID_PREFIX = ''
+AUTH_LDAP_SERVER_URI = "ldap://example.com"
+AUTH_LDAP_BIND_DN = "username@example.com"
+AUTH_LDAP_BIND_PASSWORD = "password"
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"
+)
+AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+    "CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=group)"
+)
+AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()
+AUTH_LDAP_REQUIRE_GROUP = "CN=WebVirtCloud Access,CN=Users,DC=example,DC=com"
+AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+    "is_staff": "CN=WebVirtCloud Staff,CN=Users,DC=example,DC=com",
+    "is_superuser": "CN=WebVirtCloud Admins,CN=Users,DC=example,DC=com",
+}
+AUTH_LDAP_USER_ATTR_MAP = {
+    "first_name": "givenName",
+    "last_name": "sn",
+    "email": "mail",
+}

From 1b2b3a3bceb315a746bc2758c728b66d6c6e724d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Am=C3=A9lie=20Krej=C4=8D=C3=AD?= <krejcar25@blep.cz>
Date: Sat, 23 Sep 2023 09:52:48 +0200
Subject: [PATCH 04/17] Get UserAttributes object using get_or_create

This is done to automatically create the UserAttributes object in case LDAP User Backend didn't create it.
---
 admin/views.py     | 2 +-
 instances/utils.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/admin/views.py b/admin/views.py
index fd4ead4..8b71baf 100644
--- a/admin/views.py
+++ b/admin/views.py
@@ -117,7 +117,7 @@ def user_create(request):
 @superuser_only
 def user_update(request, pk):
     user = get_object_or_404(User, pk=pk)
-    attributes = UserAttributes.objects.get(user=user)
+    attributes, attributes_created = UserAttributes.objects.get_or_create(user=user)
     user_form = forms.UserForm(request.POST or None, instance=user)
     attributes_form = forms.UserAttributesForm(
         request.POST or None, instance=attributes
diff --git a/instances/utils.py b/instances/utils.py
index 53752b6..f9f5dcf 100644
--- a/instances/utils.py
+++ b/instances/utils.py
@@ -2,7 +2,7 @@ import os
 import random
 import string
 
-from accounts.models import UserInstance
+from accounts.models import UserInstance, UserAttributes
 from appsettings.settings import app_settings
 from django.conf import settings
 from django.utils.translation import gettext_lazy as _
@@ -26,7 +26,7 @@ def get_clone_free_names(size=10):
 
 
 def check_user_quota(user, instance, cpu, memory, disk_size):
-    ua = user.userattributes
+    ua, attributes_created = UserAttributes.objects.get_or_create(user=user)
     msg = ""
 
     if user.is_superuser:

From 53d140748330011ef344215caf34d3e7d1d262a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Am=C3=A9lie=20Krej=C4=8D=C3=AD?= <krejcar25@blep.cz>
Date: Mon, 25 Sep 2023 14:40:22 +0200
Subject: [PATCH 05/17] Replace ifnotequal with if

ifequal and ifnotequal are deprecated in Django 4.0
---
 storages/templates/storage.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/storages/templates/storage.html b/storages/templates/storage.html
index 8da0c31..4b6235b 100644
--- a/storages/templates/storage.html
+++ b/storages/templates/storage.html
@@ -160,7 +160,7 @@
                                 </div> <!-- /.modal-content -->
                             </div> <!-- /.modal-dialog -->
                         </div> <!-- /.modal -->
-                        {% ifnotequal volume.type "iso" %}
+                        {% if volume.type != "iso" %}
                             <button class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#Clone{{ forloop.counter }}" title="{% trans "Clone" %}">
                                 {% bs_icon 'files' %} 
                             </button>
@@ -168,7 +168,7 @@
                             <button class="btn btn-sm btn-secondary disabled">
                                 {% bs_icon 'files' %} 
                             </button>
-                        {% endifnotequal %}
+                        {% endif %}
                     </td>  
                     <td>  
                         <form action="" method="post" role="form" aria-label="Delete volume form">{% csrf_token %}

From 3ef0fe19f874fff21fb0e04612bf358b1a151a9c Mon Sep 17 00:00:00 2001
From: Aldo Adirajasa Fathoni <aldo.alfathoni@gmail.com>
Date: Tue, 26 Sep 2023 13:21:02 +0700
Subject: [PATCH 06/17] Fix calling write() from int instead of os

---
 console/socketiod | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/console/socketiod b/console/socketiod
index a53a6f1..3c3c9b6 100755
--- a/console/socketiod
+++ b/console/socketiod
@@ -176,7 +176,7 @@ def connect(sid, environ):
     if child_pid:
         # already started child process, don't start another
         # write a new line so that when a client refresh the shell prompt is printed
-        fd.write("\n")
+        os.write(fd, str.encode("\n"))
         return
 
     # create child process attached to a pty we can read from and write to

From 84e22e4a8cdec677f157d1142560b40b0af31990 Mon Sep 17 00:00:00 2001
From: Aldo Adirajasa Fathoni <aldo.alfathoni@gmail.com>
Date: Tue, 26 Sep 2023 13:23:20 +0700
Subject: [PATCH 07/17] Ignore if pty process has exited

---
 console/socketiod | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/console/socketiod b/console/socketiod
index 3c3c9b6..55342d1 100755
--- a/console/socketiod
+++ b/console/socketiod
@@ -200,8 +200,13 @@ def disconnect(sid):
     global child_pid
 
     # kill pty process
-    os.kill(child_pid, signal.SIGKILL)
-    os.wait()
+    try:
+        os.kill(child_pid, signal.SIGKILL)
+        os.wait()
+    except ProcessLookupError:
+        pass
+    except ChildProcessError:
+        pass
 
     # reset the variables
     fd = None

From 64fdb9315d8ffbc608fbbb8492d20418b6f42b7b Mon Sep 17 00:00:00 2001
From: Aldo Adirajasa Fathoni <aldo.alfathoni@gmail.com>
Date: Tue, 26 Sep 2023 21:56:27 +0700
Subject: [PATCH 08/17] Fix socketio default host The previous value caused
 webvirtcloud to try to connect to socketiod with 0.0.0.0.

---
 webvirtcloud/settings.py.template | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index fabe94c..88146c1 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -201,7 +201,7 @@ WS_PUBLIC_PATH = "/novncd/"
 WS_CERT = None
 
 SOCKETIO_PORT = 6081
-SOCKETIO_HOST = '0.0.0.0'
+SOCKETIO_HOST = "0.0.0.0"
 
 # Socketio public host
 SOCKETIO_PUBLIC_HOST = None

From 5a211c0c566f1b9ec64a0cf356672eb776058cf3 Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Wed, 18 Oct 2023 17:34:40 -0400
Subject: [PATCH 09/17] Get MAC OUI from settings.py

---
 instances/utils.py                |  2 +-
 vrtManager/util.py                | 12 ++++++++----
 webvirtcloud/settings.py.template |  2 ++
 3 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/instances/utils.py b/instances/utils.py
index f9f5dcf..19b2111 100644
--- a/instances/utils.py
+++ b/instances/utils.py
@@ -196,7 +196,7 @@ def get_dhcp_mac_address(vname):
 
 
 def get_random_mac_address():
-    mac = "52:54:00:%02x:%02x:%02x" % (
+    mac = settings.MAC_OUI + ":%02x:%02x:%02x" % (
         random.randint(0x00, 0xFF),
         random.randint(0x00, 0xFF),
         random.randint(0x00, 0xFF),
diff --git a/vrtManager/util.py b/vrtManager/util.py
index ab47f32..81cb3d5 100644
--- a/vrtManager/util.py
+++ b/vrtManager/util.py
@@ -6,6 +6,8 @@ import string
 import libvirt
 import lxml.etree as etree
 
+from django.conf import UserSettingsHolder, settings
+
 
 def is_kvm_available(xml):
     kvm_domains = get_xml_path(xml, "//domain/@type='kvm'")
@@ -15,10 +17,12 @@ def is_kvm_available(xml):
 def randomMAC():
     """Generate a random MAC address."""
     # qemu MAC
-    oui = [0x52, 0x54, 0x00]
-
-    mac = oui + [random.randint(0x00, 0xFF), random.randint(0x00, 0xFF), random.randint(0x00, 0xFF)]
-    return ":".join(map(lambda x: "%02x" % x, mac))
+    mac = settings.MAC_OUI + ":%02x:%02x:%02x" % (
+        random.randint(0x00, 0xFF),
+        random.randint(0x00, 0xFF),
+        random.randint(0x00, 0xFF),
+    )
+    return mac
 
 
 def randomUUID():
diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index 88146c1..d3b9fd9 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -15,6 +15,8 @@ SECRET_KEY = ""
 
 DEBUG = False
 
+MAC_OUI = '52:54:10'
+
 ALLOWED_HOSTS = ["*"]
 
 CSRF_TRUSTED_ORIGINS = ['http://localhost',]

From 149044a90ccebaf7c05d33244f21f4a80872eb6e Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Wed, 18 Oct 2023 18:04:00 -0400
Subject: [PATCH 10/17] Fix templates - extends base.html

---
 accounts/templates/logout.html | 4 ++--
 templates/403.html             | 2 +-
 templates/404.html             | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/accounts/templates/logout.html b/accounts/templates/logout.html
index eb3a325..eb3b917 100644
--- a/accounts/templates/logout.html
+++ b/accounts/templates/logout.html
@@ -1,4 +1,4 @@
-{% extends "base_auth.html" %}
+{% extends "base.html" %}
 {% load i18n %}
 {% block title %}
     {% trans "WebVirtCloud" %} - {% trans "Sign Out"%}
@@ -14,4 +14,4 @@
             </div>
         </div>
     </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/templates/403.html b/templates/403.html
index 4b320b4..6b0a7e8 100644
--- a/templates/403.html
+++ b/templates/403.html
@@ -1,4 +1,4 @@
-{% extends "base_auth.html" %}
+{% extends "base.html" %}
 {% load i18n %}
 {% block title %}{% trans "403" %}{% endblock %}
 {% block content %}
diff --git a/templates/404.html b/templates/404.html
index c631d46..dd42e10 100644
--- a/templates/404.html
+++ b/templates/404.html
@@ -1,4 +1,4 @@
-{% extends "base_auth.html" %}
+{% extends "base.html" %}
 {% load i18n %}
 {% block title %}{% trans "404" %}{% endblock %}
 {% block content %}

From 31cace9994bd26b72ff03f8e96aef1f454c6b57b Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Wed, 18 Oct 2023 21:42:57 -0400
Subject: [PATCH 11/17] nginx conf - Fix for issue #577

---
 conf/nginx/webvirtcloud.conf | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/conf/nginx/webvirtcloud.conf b/conf/nginx/webvirtcloud.conf
index 3ba36ec..144431b 100644
--- a/conf/nginx/webvirtcloud.conf
+++ b/conf/nginx/webvirtcloud.conf
@@ -34,6 +34,12 @@ server {
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
     }
+    location /websockify {
+        proxy_pass http://wssocketiod;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
 }
 
 upstream wsnovncd {

From 4aafad668ba33465dd68be0b421e7bb67f877ee0 Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Thu, 26 Oct 2023 13:12:20 -0400
Subject: [PATCH 12/17] Location /websockify proxy passes to wsnovncd

---
 conf/nginx/webvirtcloud.conf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/conf/nginx/webvirtcloud.conf b/conf/nginx/webvirtcloud.conf
index 144431b..153b389 100644
--- a/conf/nginx/webvirtcloud.conf
+++ b/conf/nginx/webvirtcloud.conf
@@ -35,7 +35,7 @@ server {
         proxy_set_header Connection "upgrade";
     }
     location /websockify {
-        proxy_pass http://wssocketiod;
+        proxy_pass http://wsnovncd;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";

From 2072890fc586325a123569df705b31eca74f7060 Mon Sep 17 00:00:00 2001
From: Mark <0x6d61726b@gmail.com>
Date: Thu, 26 Oct 2023 21:53:04 +0200
Subject: [PATCH 13/17] Fixed nginx X-Forwarded-Proto to match the protocol
 ("http", "https")

---
 conf/nginx/webvirtcloud.conf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/conf/nginx/webvirtcloud.conf b/conf/nginx/webvirtcloud.conf
index 3ba36ec..f18585b 100644
--- a/conf/nginx/webvirtcloud.conf
+++ b/conf/nginx/webvirtcloud.conf
@@ -14,7 +14,7 @@ server {
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
         proxy_set_header Host $host:$server_port;
-        proxy_set_header X-Forwarded-Proto $remote_addr;
+        proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Forwarded-Ssl off;
         proxy_connect_timeout 1800;
         proxy_read_timeout 1800;

From 5c6bcbe610fae62b208272c0416ede5dc8905768 Mon Sep 17 00:00:00 2001
From: Mark <0x6d61726b@gmail.com>
Date: Thu, 26 Oct 2023 22:18:17 +0200
Subject: [PATCH 14/17] Fixes issue #614: Quick Install overrides nginx
 upstream wssocketiod port

---
 webvirtcloud.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/webvirtcloud.sh b/webvirtcloud.sh
index 3df52bd..f66cb0f 100755
--- a/webvirtcloud.sh
+++ b/webvirtcloud.sh
@@ -174,7 +174,7 @@ configure_nginx () {
   fi
 
   novncd_port_escape="$(echo -n "$novncd_port"|sed -e 's/[](){}<>=:\!\?\+\|\/\&$*.^[]/\\&/g')"
-  sed -i "s|\\(server 127.0.0.1:\\).*|\\1$novncd_port_escape;|" "$nginxfile"
+  sed -i "s|server 127.0.0.1:6080;|server 127.0.0.1:$novncd_port_escape;|" "$nginxfile"
 
 }
 

From cc867304414b5be9340aa76d7296576a4df4f1b1 Mon Sep 17 00:00:00 2001
From: Mark <0x6d61726b@gmail.com>
Date: Thu, 26 Oct 2023 22:43:27 +0200
Subject: [PATCH 15/17] Corrected X-Forwarded-Proto meaning for Gunicorn

---
 conf/nginx/webvirtcloud.conf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/conf/nginx/webvirtcloud.conf b/conf/nginx/webvirtcloud.conf
index f18585b..16128f3 100644
--- a/conf/nginx/webvirtcloud.conf
+++ b/conf/nginx/webvirtcloud.conf
@@ -14,7 +14,7 @@ server {
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
         proxy_set_header Host $host:$server_port;
-        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Proto http;
         proxy_set_header X-Forwarded-Ssl off;
         proxy_connect_timeout 1800;
         proxy_read_timeout 1800;

From 7e8c05fff93646651f3345128623bea7ef9506b3 Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Wed, 1 Nov 2023 18:08:38 -0400
Subject: [PATCH 16/17] Global setting for NIC type

---
 .../migrations/0009_alter_appsettings_id.py   | 18 ++++++++++++
 .../migrations/0010_auto_20231030_1305.py     | 28 +++++++++++++++++++
 instances/templates/create_instance_w2.html   |  6 ++--
 instances/views.py                            |  1 +
 4 files changed, 50 insertions(+), 3 deletions(-)
 create mode 100644 appsettings/migrations/0009_alter_appsettings_id.py
 create mode 100644 appsettings/migrations/0010_auto_20231030_1305.py

diff --git a/appsettings/migrations/0009_alter_appsettings_id.py b/appsettings/migrations/0009_alter_appsettings_id.py
new file mode 100644
index 0000000..5e588dc
--- /dev/null
+++ b/appsettings/migrations/0009_alter_appsettings_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.5 on 2023-10-30 17:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('appsettings', '0008_auto_20220905_1459'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='appsettings',
+            name='id',
+            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+        ),
+    ]
diff --git a/appsettings/migrations/0010_auto_20231030_1305.py b/appsettings/migrations/0010_auto_20231030_1305.py
new file mode 100644
index 0000000..606909d
--- /dev/null
+++ b/appsettings/migrations/0010_auto_20231030_1305.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.5 on 2023-10-30 17:05
+
+from django.db import migrations
+from django.utils.translation import gettext_lazy as _
+
+def add_default_settings(apps, schema_editor):
+    setting = apps.get_model("appsettings", "AppSettings")
+    db_alias = schema_editor.connection.alias
+    setting.objects.using(db_alias).bulk_create([
+        setting(35, _("VM NIC Type"), "INSTANCE_NIC_DEFAULT_TYPE", "default", "default,e1000,e1000e,rt18139,virtio", _("Change instance default NIC type"))
+    ])
+
+
+def del_default_settings(apps, schema_editor):
+    setting = apps.get_model("appsettings", "AppSettings")
+    db_alias = schema_editor.connection.alias
+    setting.objects.using(db_alias).filter(key="INSTANCE_NIC_DEFAULT_TYPE").delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('appsettings', '0009_alter_appsettings_id')
+    ]
+
+    operations = [
+        migrations.RunPython(add_default_settings,del_default_settings)
+    ]
diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html
index 91ae2ec..1cf7d62 100644
--- a/instances/templates/create_instance_w2.html
+++ b/instances/templates/create_instance_w2.html
@@ -200,7 +200,7 @@
                                                                         <div class="col-sm-7">
                                                                             <select class="form-select" name="net_model">
                                                                                 {% for model in net_models_host %}
-                                                                                <option value="{{ model }}" {% if model == 'default'  %} selected {% endif %}>{{ model }}</option>
+                                                                                <option value="{{ model }}" {% if model == default_nic_type %} selected {% endif %}>{{ model }}</option>
                                                                                 {% endfor %}
                                                                             </select>
                                                                         </div>
@@ -476,7 +476,7 @@
                             <div class="col-sm-7">
                                 <select class="form-select" name="net_model">
                                     {% for model in net_models_host %}
-                                    <option value="{{ model }}" {% if model == 'default'  %} selected {% endif %}>{{ model }}</option>
+                                    <option value="{{ model }}" {% if model == default_nic_type %} selected {% endif %}>{{ model }}</option>
                                     {% endfor %}
                                 </select>
                             </div>
@@ -728,7 +728,7 @@
                                 <div class="col-sm-7">
                                     <select class="form-select" name="net_model">
                                         {% for model in net_models_host %}
-                                        <option value="{{ model }}" {% if model == 'default'  %} selected {% endif %}>{{ model }}</option>
+                                        <option value="{{ model }}" {% if model == default_nic_type %} selected {% endif %}>{{ model }}</option>
                                         {% endfor %}
                                     </select>
                                 </div>
diff --git a/instances/views.py b/instances/views.py
index 57d1598..cfaa157 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -1692,6 +1692,7 @@ def create_instance(request, compute_id, arch, machine):
         networks = sorted(conn.get_networks())
         nwfilters = conn.get_nwfilters()
         net_models_host = conn.get_network_models()
+        default_nic_type = app_settings.INSTANCE_NIC_DEFAULT_TYPE
         storages = sorted(conn.get_storages(only_actives=True))
         default_graphics = app_settings.QEMU_CONSOLE_DEFAULT_TYPE
         default_cdrom = app_settings.INSTANCE_CDROM_ADD

From 2f24f77368af48f3353aa14d030bd7cfadc33e6f Mon Sep 17 00:00:00 2001
From: MisterBlueBear <51129551+MisterBlueBear@users.noreply.github.com>
Date: Wed, 1 Nov 2023 20:35:15 -0400
Subject: [PATCH 17/17] Install script accepts default hostname

---
 webvirtcloud.sh | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/webvirtcloud.sh b/webvirtcloud.sh
index f66cb0f..b846b11 100755
--- a/webvirtcloud.sh
+++ b/webvirtcloud.sh
@@ -424,9 +424,15 @@ until [[ $setupfqdn == "yes" ]] || [[ $setupfqdn == "no" ]]; do
 
   case $setupfqdn in
     [yY] | [yY][Ee][Ss] )
-    echo -n "  Q. What is the FQDN of your server? ($(hostname --fqdn)): "
-      read -r fqdn
+    fqdn=$(hostname --fqdn)
+    echo -n "  Q. What is the FQDN of your server? ($fqdn): "
+      read -r fqdn_from_user
       setupfqdn="yes"
+
+      if [ ! -z $fqdn_from_user ]; then
+        fqdn=$fqdn_from_user
+      fi
+
       echo "     Setting to $fqdn"
       echo ""
       ;;