diff --git a/app/models.py b/app/models.py
index 9d34663786d3d4bca964b8d116c8ab5aa43a4cd3..052fae93cba42557cbe4ef60e5637887294a905e 100644
--- a/app/models.py
+++ b/app/models.py
@@ -301,6 +301,29 @@ class User(db.Model, UserMixin):
             in current_app.config["ALLOWED_VM_CREATION_DOMAINS"]
         )
 
+    def can_set_boot_profile(self, host):
+        """Return True if the user can set the network boot profile
+
+        - host.device_type shall be PhysicalMachine
+        - admin users can always set the profile
+        - normal users must have access to the network
+        - normal users can only set the boot profile if the host is in one of the allowed domains
+        - LOGIN_DISABLED can be set to True to turn off authentication check when testing.
+          In this case, this function always returns True.
+        """
+        if str(host.device_type) != "PhysicalMachine":
+            return False
+        if current_app.config.get("LOGIN_DISABLED") or self.is_admin:
+            return True
+        if not self.has_access_to_network(host.main_network):
+            # True is already returned for admin users
+            return False
+        # Boot profile can only be set if the domain is allowed
+        return (
+            str(host.main_interface.network.domain)
+            in current_app.config["ALLOWED_SET_BOOT_PROFILE_DOMAINS"]
+        )
+
     def favorite_attributes(self):
         """Return all user's favorite attributes"""
         favorites_list = [
diff --git a/app/network/forms.py b/app/network/forms.py
index deb988edfcae2c8a18b36a0eef3ee8263529977a..256875aeb0eddb47da45827d5c27d62d89605223 100644
--- a/app/network/forms.py
+++ b/app/network/forms.py
@@ -238,6 +238,16 @@ class CreateVMForm(CSEntryForm):
         )
 
 
+class BootProfileForm(CSEntryForm):
+    boot_profile = SelectField("Boot profile")
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.boot_profile.choices = utils.get_choices(
+            current_app.config["AUTOINSTALL_BOOT_PROFILES"]
+        )
+
+
 class GenerateZTPConfigForm(CSEntryForm):
     pass
 
diff --git a/app/network/views.py b/app/network/views.py
index d21f1ad6333b2f3fafcc656a98dbc79642b5224a..fcef45e6ab647cfe670ac94a719075562f695c0d 100644
--- a/app/network/views.py
+++ b/app/network/views.py
@@ -35,6 +35,7 @@ from .forms import (
     CreateVMForm,
     GenerateZTPConfigForm,
     AnsibleGroupForm,
+    BootProfileForm,
 )
 from ..extensions import db
 from ..decorators import login_groups_accepted
@@ -148,6 +149,8 @@ def view_host(name):
         )
     if host.device_type.name == "Network":
         form = GenerateZTPConfigForm()
+    elif host.device_type.name == "PhysicalMachine":
+        form = BootProfileForm()
     elif host.device_type.name.startswith("Virtual"):
         form = CreateVMForm()
         if host.is_ioc:
@@ -177,6 +180,28 @@ def view_host(name):
                     "success",
                 )
                 return redirect(url_for("task.view_task", id_=task.id))
+        elif host.device_type.name == "PhysicalMachine":
+            if not current_user.can_set_boot_profile(host):
+                flash(
+                    f"You don't have the proper permissions to set the boot profile. Please contact an admin user.",
+                    "warning",
+                )
+                return redirect(url_for("network.view_host", name=name))
+            else:
+                boot_profile = form.boot_profile.data
+                task = utils.trigger_set_network_boot_profile(
+                    host, boot_profile=boot_profile
+                )
+                db.session.commit()
+                current_app.logger.info(
+                    f"Set network boot profile to {boot_profile} for {name} requested: task {task.id}"
+                )
+                flash(
+                    f"Set network boot profile to {boot_profile} for {name} requested! "
+                    "Refresh the page to update the status. You can reboot the machine when the task is done.",
+                    "success",
+                )
+                return redirect(url_for("task.view_task", id_=task.id))
         else:
             if not current_user.can_create_vm(host):
                 flash(
diff --git a/app/settings.py b/app/settings.py
index 5c1ab337d3bd5af79723e5e3f4c40063e40d6af9..fd7af2898ac5d11089ab2cf843c150e72aece375 100644
--- a/app/settings.py
+++ b/app/settings.py
@@ -74,6 +74,11 @@ CSENTRY_DOMAINS_LDAP_GROUPS = {
 }
 # List of domains where users can create a VM
 ALLOWED_VM_CREATION_DOMAINS = ["cslab.esss.lu.se"]
+# List of domains where users can set the boot profile for physical machines
+ALLOWED_SET_BOOT_PROFILE_DOMAINS = ["cslab.esss.lu.se"]
+# List of existing boot profiles
+# Shall be kept in sync with the ics-ans-role-autoinstall Ansible role
+AUTOINSTALL_BOOT_PROFILES = ["localboot", "default", "cct", "LCR", "thinclient"]
 
 NETWORK_DEFAULT_PREFIX = 24
 # ICS Ids starting with this prefix are considered temporary and can be changed
@@ -112,6 +117,7 @@ AWX_POST_INSTALL = {
         "cslab.esss.lu.se": "customize-LabVM",
     },
 }
+AWX_SET_NETWORK_BOOT_PROFILE = "deploy-autoinstall_server@setboot"
 
 AWX_JOB_ENABLED = False
 AWX_VM_CREATION_ENABLED = False
diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html
index 240ba550f6b066a55168a8eb9de2d6040269ae34..97397c3c87fcb11fad73d19c16cf572f5bb643a2 100644
--- a/app/templates/network/view_host.html
+++ b/app/templates/network/view_host.html
@@ -92,6 +92,14 @@
           {{ submit_button_with_confirmation('Generate ZTP configuration', 'Do you really want to generate the ZTP configuration for ' + host.name + '?') }}
         </form>
       </div>
+    {% elif host.device_type.name == 'PhysicalMachine' %}
+      <div class="col-sm-4">
+        <form id="BootProfileForm" method="POST">
+          {{ form.hidden_tag() }}
+          {{ render_field(form.boot_profile, label_size='5', input_size='7') }}
+          {{ submit_button_with_confirmation('Set boot profile', 'Do you really want to set the boot profile for ' + host.name + '?') }}
+        </form>
+      </div>
     {% endif %}
   </div>
   <h3>Interfaces</h3>
diff --git a/app/utils.py b/app/utils.py
index 82b64c1777e36251edbf4652e60603ddcf4d6979..c1f2c663ea1d6763b42674d3eefad39332a57c12 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -400,6 +400,28 @@ def trigger_ztp_configuration(host):
     return task
 
 
+def trigger_set_network_boot_profile(host, boot_profile):
+    """Trigger a job to set the boot profile for host"""
+    extra_vars = [
+        f"autoinstall_boot_profile={boot_profile}",
+        f"autoinstall_pxe_mac_addr={host.main_interface.mac}",
+    ]
+    job_template = current_app.config["AWX_SET_NETWORK_BOOT_PROFILE"]
+    current_app.logger.info(
+        f"Launch new job to set the network boot profile for {host.name}: {job_template} with {extra_vars}"
+    )
+    task = current_user.launch_task(
+        "trigger_set_network_boot_profile",
+        resource="job",
+        queue_name="low",
+        func="launch_awx_job",
+        job_template=job_template,
+        extra_vars=extra_vars,
+        timeout=500,
+    )
+    return task
+
+
 def redirect_to_job_status(job_id):
     """
     The answer to a client request, leading it to regularly poll a job status.
diff --git a/docs/_static/network/set_boot_profile.png b/docs/_static/network/set_boot_profile.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c34760b13ba372ce57a657688a9562ac0b14d78
Binary files /dev/null and b/docs/_static/network/set_boot_profile.png differ
diff --git a/docs/_static/network/set_boot_profile_confirmation.png b/docs/_static/network/set_boot_profile_confirmation.png
new file mode 100644
index 0000000000000000000000000000000000000000..337f35c0e66868821b4fd63e8a7238f0f9ffdec6
Binary files /dev/null and b/docs/_static/network/set_boot_profile_confirmation.png differ
diff --git a/docs/network.rst b/docs/network.rst
index 386b9839caa04d613b0998bca0b63a9c269f3c0b..6b30fe9e10b4479c4eb56b5aa1d5d9576c005013 100644
--- a/docs/network.rst
+++ b/docs/network.rst
@@ -152,6 +152,25 @@ A VIOC can be created by any user who has access to the associated network/domai
 The same restriction applies to VMs but the associated domain must also be part of the **ALLOWED_VM_CREATION_DOMAINS** list (currently cslab.tn.esss.lu.se).
 Please contact an admin user if you don't have the proper permissions.
 
+Physical Machine installation
+-----------------------------
+
+From the *View host* page, you can set the boot profile of a Physical Machine. This can be used to perform a network installation:
+
+.. image:: _static/network/set_boot_profile.png
+
+When submitting the form, a confirmation dialog will be displayed.
+
+.. image:: _static/network/set_boot_profile_confirmation.png
+
+This will trigger a job template on AWX (**AWX_SET_NETWORK_BOOT_PROFILE**) that creates a link for the host MAC address to the proper profile.
+When the task is done, the machine can be rebooted. If it boots on the network, the chosen installation will be performed.
+After the installation the boot profile is automatically set back to **localboot**. So even if the machine is rebooted, it will boot locally.
+
+The boot profile can be set by any user who has access to the associated network/domain.
+But the domain must also be part of the **ALLOWED_SET_BOOT_PROFILE_DOMAINS** list (currently cslab.tn.esss.lu.se) for non admin users.
+Please contact an admin user if you don't have the proper permissions.
+
 Ansible inventory
 -----------------
 
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 9976b9913d1634c82745a6139792fa6307c98d81..1504e4fb9ffdd862a1460c3964e3261d220899fa 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -60,6 +60,7 @@ def app(request):
         },
         "AWX_URL": "https://awx.example.org",
         "ALLOWED_VM_CREATION_DOMAINS": ["lab.example.org"],
+        "ALLOWED_SET_BOOT_PROFILE_DOMAINS": ["lab.example.org"],
         "MAX_PER_PAGE": 25,
     }
     app = create_app(config=config)
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index 71ebc48dcc35f306ee6e21f534ce6de354d23920..d3448e0ae6660bb0d5b33237588a3cf4d5bccb11 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -154,6 +154,85 @@ def test_user_can_create_vm(
     assert not user.can_create_vm(non_vm_ioc)
 
 
+def test_user_can_set_boot_profile(
+    user_factory,
+    domain_factory,
+    network_factory,
+    device_type_factory,
+    host_factory,
+    interface_factory,
+):
+    physicalmachine = device_type_factory(name="PhysicalMachine")
+    domain_prod = domain_factory(name="prod.example.org")
+    domain_lab = domain_factory(name="lab.example.org")
+    network_prod = network_factory(domain=domain_prod)
+    network_lab = network_factory(domain=domain_lab)
+    network_lab_admin = network_factory(domain=domain_lab, admin_only=True)
+    server_prod = host_factory(device_type=physicalmachine)
+    interface_factory(name=server_prod.name, host=server_prod, network=network_prod)
+    ioc_prod = host_factory(device_type=physicalmachine, is_ioc=True)
+    interface_factory(name=ioc_prod.name, host=ioc_prod, network=network_prod)
+    server_lab = host_factory(device_type=physicalmachine)
+    interface_factory(name=server_lab.name, host=server_lab, network=network_lab)
+    ioc_lab = host_factory(device_type=physicalmachine, is_ioc=True)
+    interface_factory(name=ioc_lab.name, host=ioc_lab, network=network_lab)
+    server_lab_admin = host_factory(device_type=physicalmachine)
+    interface_factory(
+        name=server_lab_admin.name, host=server_lab_admin, network=network_lab_admin
+    )
+    ioc_lab_admin = host_factory(device_type=physicalmachine, is_ioc=True)
+    interface_factory(
+        name=ioc_lab_admin.name, host=ioc_lab_admin, network=network_lab_admin
+    )
+    non_physical = host_factory()
+    non_physical_ioc = host_factory(is_ioc=True)
+    interface_factory(name=non_physical.name, host=non_physical, network=network_lab)
+    interface_factory(
+        name=non_physical_ioc.name, host=non_physical_ioc, network=network_lab
+    )
+    # User has access to prod and lab networks but can only set the boot profile in the lab
+    # (due to ALLOWED_SET_BOOT_PROFILE_DOMAINS)
+    user = user_factory(groups=["CSEntry Prod", "CSEntry Lab"])
+    assert user.can_set_boot_profile(server_lab)
+    assert not user.can_set_boot_profile(server_prod)
+    assert not user.can_set_boot_profile(server_lab_admin)
+    assert not user.can_set_boot_profile(non_physical)
+    assert user.can_set_boot_profile(ioc_lab)
+    assert not user.can_set_boot_profile(ioc_prod)
+    assert not user.can_set_boot_profile(ioc_lab_admin)
+    assert not user.can_set_boot_profile(non_physical_ioc)
+    # User has only access to the lab networks and can only set the boot profile in the lab
+    user = user_factory(groups=["foo", "CSEntry Lab"])
+    assert user.can_set_boot_profile(server_lab)
+    assert not user.can_set_boot_profile(server_prod)
+    assert not user.can_set_boot_profile(server_lab_admin)
+    assert not user.can_set_boot_profile(non_physical)
+    assert user.can_set_boot_profile(ioc_lab)
+    assert not user.can_set_boot_profile(ioc_prod)
+    assert not user.can_set_boot_profile(ioc_lab_admin)
+    assert not user.can_set_boot_profile(non_physical_ioc)
+    # User can't set the boot profile
+    user = user_factory(groups=["one", "two"])
+    assert not user.can_set_boot_profile(server_lab)
+    assert not user.can_set_boot_profile(server_prod)
+    assert not user.can_set_boot_profile(server_lab_admin)
+    assert not user.can_set_boot_profile(non_physical)
+    assert not user.can_set_boot_profile(ioc_lab)
+    assert not user.can_set_boot_profile(ioc_prod)
+    assert not user.can_set_boot_profile(ioc_lab_admin)
+    assert not user.can_set_boot_profile(non_physical_ioc)
+    # Admin can set the boot profile on all physical machines
+    user = user_factory(groups=["CSEntry Admin"])
+    assert user.can_set_boot_profile(server_lab)
+    assert user.can_set_boot_profile(server_prod)
+    assert user.can_set_boot_profile(server_lab_admin)
+    assert not user.can_set_boot_profile(non_physical)
+    assert user.can_set_boot_profile(ioc_lab)
+    assert user.can_set_boot_profile(ioc_prod)
+    assert user.can_set_boot_profile(ioc_lab_admin)
+    assert not user.can_set_boot_profile(non_physical_ioc)
+
+
 def test_network_ip_properties(network_factory):
     # Create some networks
     network1 = network_factory(