diff --git a/alts/shared/models.py b/alts/shared/models.py index 9418d163..e32c4db5 100644 --- a/alts/shared/models.py +++ b/alts/shared/models.py @@ -154,6 +154,8 @@ class OpennebulaConfig(BaseModel): vm_group: Optional[str] = None default_vm_disk_size: Optional[int] = 15360 default_vm_ram_size: Optional[int] = 1536 + # Swapfile size in MB created during provisioning; 0 disables it + default_vm_swap_size: Optional[int] = 0 network: Optional[str] = None # Quota checking configuration quota_check_enabled: bool = False diff --git a/alts/worker/runners/base.py b/alts/worker/runners/base.py index b159c51f..a80d20bd 100644 --- a/alts/worker/runners/base.py +++ b/alts/worker/runners/base.py @@ -356,6 +356,13 @@ def vm_ram_size(self) -> int: CONFIG.opennebula_config.default_vm_ram_size, ) + @property + def vm_swap_size(self) -> int: + return self._test_env.get( + 'vm_swap_size', + getattr(CONFIG.opennebula_config, 'default_vm_swap_size', 0), + ) + @property def dist_arch(self): return self._dist_arch @@ -745,6 +752,7 @@ def initial_provision(self, verbose=False): 'integrity_tests_dir': self._integrity_tests_dir, 'connection_type': self.ansible_connection_type, 'pytest_is_needed': self.pytest_is_needed, + 'vm_swap_size': self.vm_swap_size, 'development_mode': CONFIG.development_mode, 'package_proxy': CONFIG.package_proxy, 'third_party_repo_ssh_hosts': [ diff --git a/alts/worker/runners/opennebula.py b/alts/worker/runners/opennebula.py index c95f9401..b5f99d9d 100644 --- a/alts/worker/runners/opennebula.py +++ b/alts/worker/runners/opennebula.py @@ -203,6 +203,7 @@ def _render_tf_main_file(self): template_id=template_id, vm_disk_size=self.vm_disk_size, vm_ram_size=self.vm_ram_size, + vm_swap_size=self.vm_swap_size, opennebula_network=CONFIG.opennebula_config.network, ) diff --git a/configs/example_config.yaml b/configs/example_config.yaml index 4da4fe18..620479e6 100644 --- a/configs/example_config.yaml +++ b/configs/example_config.yaml @@ -42,6 +42,7 @@ opennebula_config: # in MB default_vm_disk_size: default_vm_ram_size: + default_vm_swap_size: network: bs_host: diff --git a/resources/opennebula/opennebula.tf.tmpl b/resources/opennebula/opennebula.tf.tmpl index 2c02f6d6..e7fb8243 100644 --- a/resources/opennebula/opennebula.tf.tmpl +++ b/resources/opennebula/opennebula.tf.tmpl @@ -36,9 +36,9 @@ locals { # Disk configuration disk_0 = data.opennebula_template.selected_template.disk[0] disk_size = max( - local.disk_0.size, # Original disk size from template - 15360, # Minimum required size - local.disk_0.size + 5120 # Original size + 5GB + local.disk_0.size, # Original disk size from template + 15360, # Minimum required size + local.disk_0.size + 5120 + ${vm_swap_size} # Original size + 5GB + swapfile (MB) ) # Determine network_id: use template network if available, otherwise use test_system_network network_id = local.template_has_network ? data.opennebula_virtual_network.template_network[0].id : data.opennebula_virtual_network.test_system_network.id diff --git a/resources/roles/preparation/defaults/main.yml b/resources/roles/preparation/defaults/main.yml index 769f8927..d804063f 100644 --- a/resources/roles/preparation/defaults/main.yml +++ b/resources/roles/preparation/defaults/main.yml @@ -10,3 +10,6 @@ third_party_repo_ssh_hosts: [] # and is passed in as an extra-var by the runner. Empty list here is a # no-op fallback so the role still works if invoked standalone. cached_test_repos: [] +# Swapfile size in MB, passed in as an extra-var by the runner from the +# platform's `vm_swap_size`. 0 (the default) disables swapfile creation. +vm_swap_size: 0 diff --git a/resources/roles/preparation/tasks/main.yml b/resources/roles/preparation/tasks/main.yml index fa95094e..38142046 100644 --- a/resources/roles/preparation/tasks/main.yml +++ b/resources/roles/preparation/tasks/main.yml @@ -21,6 +21,45 @@ tags: - initial_provision +# Some platforms (e.g. CL7h) carry many available kernels across repos, and +# dnf/yum can exhaust memory while resolving kernel-module dependencies. +# A swapfile sized from the platform's `vm_swap_size` gives them headroom. +# VM-only (Docker envs share the host kernel and can't swapon) and a no-op +# when vm_swap_size is 0. +- name: Provide swap space for memory-constrained platforms + when: + - connection_type != 'docker' + - (vm_swap_size | int) > 0 + tags: + - initial_provision + block: + - name: Allocate swapfile + ansible.builtin.command: + cmd: "fallocate -l {{ vm_swap_size }}M /swapfile" + creates: /swapfile + + - name: Restrict swapfile permissions + ansible.builtin.file: + path: /swapfile + owner: root + group: root + mode: '0600' + + - name: Format swapfile + ansible.builtin.command: + cmd: mkswap /swapfile + register: _mkswap + changed_when: _mkswap.rc == 0 + + - name: Enable swapfile + ansible.builtin.command: + cmd: swapon /swapfile + register: _swapon + changed_when: _swapon.rc == 0 + failed_when: + - _swapon.rc != 0 + - "'already active' not in _swapon.stderr" + # SSH client config is only needed where the third-party repo is cloned # *inside* the environment (VMs cloned over SSH). Docker envs receive the # repo via `docker cp` from the worker, so they never clone and need no