From b2242ec849f1cbd2018b824d8a7529a9aedfc5a9 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 25 Nov 2025 13:11:02 -0500 Subject: [PATCH] Fix Scaleway deployment by replacing broken organization_info module (fixes #14846) (#14848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Scaleway deployment by replacing broken organization_info module (fixes #14846) The scaleway_organization_info Ansible module is broken and returns empty data due to using a deprecated API endpoint (upstream issue ansible-collections/community.general#3782). This causes deployments to fail with "'NoneType' object has no attribute 'get'" error. Changes: - Replace broken scaleway_organization_info module with user prompt for Organization/Project ID - Use Scaleway Marketplace API for image lookup instead of broken scaleway_image_info module - Migrate from deprecated 'organization' parameter to modern 'project' parameter in scaleway_compute calls - Add support for SCW_DEFAULT_ORGANIZATION_ID environment variable - Provide clear instructions for finding Organization ID in console Technical details: - Scaleway's default project ID equals the organization ID - Marketplace API is public and doesn't require authentication - The 'project' parameter was added in community.general 4.3.0 Testing: - Added 4 new unit tests validating the fix - All 91 unit tests pass - Passes ansible-lint, yamllint, ruff, and shellcheck 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Suppress CodeQL false positives in Scaleway tests The CodeQL scanner flags domain string assertions as "incomplete URL substring sanitization" but these are test assertions checking that Ansible role files reference the correct URLs, not URL validation logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Revert "Suppress CodeQL false positives in Scaleway tests" This reverts commit c75a73e3f52ae22ab256744f7ab1352b1850ed29. --------- Co-authored-by: Claude --- roles/cloud-scaleway/tasks/main.yml | 38 ++++--- roles/cloud-scaleway/tasks/prompts.yml | 15 +++ tests/unit/test_scaleway_fix.py | 136 +++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 tests/unit/test_scaleway_fix.py diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 1239a949..646bd48d 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -3,25 +3,21 @@ import_tasks: prompts.yml - block: - - name: Gather Scaleway organizations facts - scaleway_organization_info: - register: scaleway_org + - name: Get Ubuntu 22.04 image ID from Scaleway Marketplace API + uri: + url: "https://api-marketplace.scaleway.com/images?arch={{ cloud_providers.scaleway.arch }}&include_eol=false" + method: GET + return_content: true + register: marketplace_images - - name: Get images - scaleway_image_info: - region: "{{ algo_region }}" - register: scaleway_image - - - name: Set cloud specific facts + - name: Find Ubuntu 22.04 Jammy image set_fact: - organization_id: "{{ scaleway_org.scaleway_organization_info[0]['id'] }}" - images: >- - [{% for i in scaleway_image.scaleway_image_info -%} - {% if i.name == cloud_providers.scaleway.image and - i.arch == cloud_providers.scaleway.arch -%} - '{{ i.id }}'{% if not loop.last %},{% endif %} - {%- endif -%} - {%- endfor -%}] + scaleway_image_id: >- + {{ (marketplace_images.json.images | + selectattr('name', 'match', '.*Ubuntu.*22\\.04.*Jammy.*') | + first).versions[0].local_images | + selectattr('zone', 'equalto', algo_region) | + map(attribute='id') | first }} - name: Create a server scaleway_compute: @@ -30,8 +26,8 @@ public_ip: dynamic boot_type: local state: present - image: "{{ images[0] }}" - organization: "{{ organization_id }}" + image: "{{ scaleway_image_id }}" + project: "{{ algo_scaleway_org_id }}" region: "{{ algo_region }}" commercial_type: "{{ cloud_providers.scaleway.size }}" wait: true @@ -57,8 +53,8 @@ public_ip: dynamic boot_type: local state: running - image: "{{ images[0] }}" - organization: "{{ organization_id }}" + image: "{{ scaleway_image_id }}" + project: "{{ algo_scaleway_org_id }}" region: "{{ algo_region }}" commercial_type: "{{ cloud_providers.scaleway.size }}" wait: true diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml index 015ab60c..f5f45efd 100644 --- a/roles/cloud-scaleway/tasks/prompts.yml +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -21,6 +21,20 @@ register: _algo_region when: region is undefined +- pause: + prompt: | + Enter your Scaleway Organization ID (also serves as your default Project ID) + You can find this in your Scaleway console: + 1. Go to https://console.scaleway.com/organization/settings + 2. Copy the Organization ID from the Organization Settings page + + Note: For the default project, the Project ID is the same as the Organization ID. + (https://trailofbits.github.io/algo/cloud-scaleway.html) + register: _scaleway_org_id + when: + - scaleway_org_id is undefined + - lookup('env', 'SCW_DEFAULT_ORGANIZATION_ID')|length <= 0 + - name: Set scaleway facts set_fact: algo_scaleway_token: "{{ scaleway_token | default(_scaleway_token.user_input) | default(lookup('env', 'SCW_TOKEN'), true) }}" @@ -28,4 +42,5 @@ {% if region is defined %}{{ region }} {%- elif _algo_region.user_input %}{{ scaleway_regions[_algo_region.user_input | int - 1]['alias'] }} {%- else %}{{ scaleway_regions.0.alias }}{% endif %} + algo_scaleway_org_id: "{{ scaleway_org_id | default(_scaleway_org_id.user_input) | default(lookup('env', 'SCW_DEFAULT_ORGANIZATION_ID'), true) }}" no_log: true diff --git a/tests/unit/test_scaleway_fix.py b/tests/unit/test_scaleway_fix.py new file mode 100644 index 00000000..41cf63e8 --- /dev/null +++ b/tests/unit/test_scaleway_fix.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Test Scaleway role fixes for issue #14846 + +This test validates that: +1. The Scaleway role uses the modern 'project' parameter instead of deprecated 'organization' +2. The Marketplace API is used for image lookup instead of the broken scaleway_image_info module +3. The prompts include organization/project ID collection +""" + +import sys +from pathlib import Path + +import yaml + + +def load_yaml_file(file_path): + """Load and parse a YAML file""" + with open(file_path) as f: + return yaml.safe_load(f) + + +def test_scaleway_main_uses_project_parameter(): + """Test that main.yml uses 'project' instead of deprecated 'organization' parameter""" + main_yml = Path("roles/cloud-scaleway/tasks/main.yml") + assert main_yml.exists(), "Scaleway main.yml not found" + + with open(main_yml) as f: + content = f.read() + + # Should NOT use the broken scaleway_organization_info module + assert ( + "scaleway_organization_info" not in content + ), "Still using broken scaleway_organization_info module (issue #14846)" + + # Should NOT use the broken scaleway_image_info module + assert "scaleway_image_info" not in content, "Still using broken scaleway_image_info module" + + # Should use project parameter (modern approach) + assert "project:" in content, "Missing 'project:' parameter in scaleway_compute calls" + assert "algo_scaleway_org_id" in content, "Missing algo_scaleway_org_id variable reference" + + # Should NOT use deprecated organization parameter + assert 'organization: "{{' not in content, "Still using deprecated 'organization' parameter" + + # Should use Marketplace API for image lookup + assert "api-marketplace.scaleway.com" in content, "Not using Scaleway Marketplace API for image lookup" + + print("✓ Scaleway main.yml uses modern 'project' parameter") + + +def test_scaleway_prompts_collect_org_id(): + """Test that prompts.yml collects organization/project ID from user""" + prompts_yml = Path("roles/cloud-scaleway/tasks/prompts.yml") + assert prompts_yml.exists(), "Scaleway prompts.yml not found" + + with open(prompts_yml) as f: + content = f.read() + + # Should prompt for organization ID + assert "Organization ID" in content, "Missing prompt for Scaleway Organization ID" + + # Should set algo_scaleway_org_id fact + assert "algo_scaleway_org_id:" in content, "Missing algo_scaleway_org_id fact definition" + + # Should support SCW_DEFAULT_ORGANIZATION_ID env var + assert ( + "SCW_DEFAULT_ORGANIZATION_ID" in content + ), "Missing support for SCW_DEFAULT_ORGANIZATION_ID environment variable" + + # Should mention console.scaleway.com for finding the ID + assert "console.scaleway.com" in content, "Missing instructions on where to find Organization ID" + + print("✓ Scaleway prompts.yml collects organization/project ID") + + +def test_scaleway_config_has_valid_settings(): + """Test that config.cfg has valid Scaleway settings""" + config_file = Path("config.cfg") + assert config_file.exists(), "config.cfg not found" + + with open(config_file) as f: + content = f.read() + + # Should have scaleway section + assert "scaleway:" in content, "Missing Scaleway configuration section" + + # Should specify Ubuntu 22.04 + assert "Ubuntu 22.04" in content or "ubuntu" in content.lower(), "Missing Ubuntu image specification" + + print("✓ config.cfg has valid Scaleway settings") + + +def test_scaleway_marketplace_api_usage(): + """Test that the role correctly uses Scaleway Marketplace API""" + main_yml = Path("roles/cloud-scaleway/tasks/main.yml") + + with open(main_yml) as f: + content = f.read() + + # Should use uri module to fetch from Marketplace API + assert "uri:" in content, "Not using uri module for API calls" + + # Should filter for Ubuntu 22.04 Jammy + assert "Ubuntu" in content and "22" in content, "Not filtering for Ubuntu 22.04 image" + + # Should set scaleway_image_id variable + assert "scaleway_image_id" in content, "Missing scaleway_image_id variable for image UUID" + + print("✓ Scaleway role uses Marketplace API correctly") + + +if __name__ == "__main__": + tests = [ + test_scaleway_main_uses_project_parameter, + test_scaleway_prompts_collect_org_id, + test_scaleway_config_has_valid_settings, + test_scaleway_marketplace_api_usage, + ] + + failed = 0 + for test in tests: + try: + test() + except AssertionError as e: + print(f"✗ {test.__name__} failed: {e}") + failed += 1 + except Exception as e: + print(f"✗ {test.__name__} error: {e}") + failed += 1 + + if failed > 0: + print(f"\n{failed} tests failed") + sys.exit(1) + else: + print(f"\nAll {len(tests)} tests passed!")