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(