Skip to content

sap_software_download: New role for downloading software #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# Collection wide lint-file
# DO NOT CHANGE
exclude_paths:
- .ansible/
- .cache/
- .github/
# - docs/
- changelogs/
- playbooks/
- tests/
enable_list:
- yaml
skip_list:
# We don't want to enforce new Ansible versions for Galaxy:
- meta-runtime[unsupported-version]
# We do not want to use checks which are marked as experimental:
- experimental
# We use ignore_errors for all the assert tasks, which should be acceptable:
- ignore-errors
# We want to allow single digit version numbers in a role's meta/main.yml file:
- schema
# Allow templating inside name because it creates more detailed output:
- name[template]
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ __pycache__/
# VSCode
.vscode

.ansible
20 changes: 17 additions & 3 deletions plugins/module_utils/sap_api_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,23 @@ def _request(url, **kwargs):
method = 'POST' if kwargs.get('data') or kwargs.get('json') else 'GET'
res = https_session.request(method, url, **kwargs)

if (res.status_code == 403
and res.json()['errorMessage'].startswith('Account Temporarily Locked Out')):
raise Exception('SAP ID Service has reported `Account Temporarily Locked Out`. Please reset password to regain access and try again.')
# Validating against `res.text` can cause long execution time, because fuzzy search result can contain large `res.text`.
# This can be prevented by validating `res.status_code` check before `res.text`.
# Example: 'Two-Factor Authentication' is only in `res.text`, which can lead to long execution.

if res.status_code == 403:
if 'You are not authorized to download this file' in res.text:
raise Exception(f'You are not authorized to download this file.')
elif 'Account Temporarily Locked Out' in res.text:
raise Exception(f'Account Temporarily Locked Out. Please reset password to regain access and try again.')
else:
res.raise_for_status()

if res.status_code == 404:
if 'The file you have requested cannot be found' in res.text:
raise Exception(f'The file you have requested cannot be found.')
else:
res.raise_for_status()

res.raise_for_status()

Expand Down
10 changes: 5 additions & 5 deletions plugins/modules/maintenance_planner_stack_xml_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- Transaction Name or Transaction Display ID from Maintenance Planner.
required: true
type: str
download_path:
dest:
description:
- Destination folder path.
required: true
Expand All @@ -46,7 +46,7 @@
suser_id: 'SXXXXXXXX'
suser_password: 'password'
transaction_name: 'MP_NEW_INST_20211015_044854'
download_path: "/tmp/"
dest: "/tmp/"
register: sap_mp_register
- name: Display the list of download links and filenames
debug:
Expand Down Expand Up @@ -79,7 +79,7 @@ def run_module():
suser_id=dict(type='str', required=True),
suser_password=dict(type='str', required=True, no_log=True),
transaction_name=dict(type='str', required=True),
download_path=dict(type='str', required=True)
dest=dict(type='str', required=True)
)

# Define result dictionary objects to be passed back to Ansible
Expand All @@ -102,7 +102,7 @@ def run_module():
username = module.params.get('suser_id')
password = module.params.get('suser_password')
transaction_name = module.params.get('transaction_name')
download_path = module.params.get('download_path')
dest = module.params.get('dest')

# Main run

Expand All @@ -118,7 +118,7 @@ def run_module():
transaction_id = get_transaction_id(transaction_name)

# EXEC: Download the MP Stack XML file
get_transaction_stack_xml(transaction_id, download_path)
get_transaction_stack_xml(transaction_id, dest)

# Process return dictionary for Ansible
result['changed'] = True
Expand Down
32 changes: 16 additions & 16 deletions plugins/modules/software_center_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
- Download filename of the SAP software.
required: false
type: str
download_path:
dest:
description:
- Destination folder path.
required: true
Expand Down Expand Up @@ -80,14 +80,14 @@
suser_password: 'password'
search_query:
- 'SAPCAR_1324-80000936.EXE'
download_path: "/tmp/"
dest: "/tmp/"
- name: Download using direct link and filename
community.sap_launchpad.software_center_download:
suser_id: 'SXXXXXXXX'
suser_password: 'password'
download_link: 'https://softwaredownloads.sap.com/file/0010000000048502015'
download_filename: 'IW_FNDGC100.SAR'
download_path: "/tmp/"
dest: "/tmp/"
'''

RETURN = r'''
Expand Down Expand Up @@ -116,21 +116,21 @@
from ..module_utils.sap_launchpad_software_center_download_runner import *
from ..module_utils.sap_id_sso import sap_sso_login

def _check_similar_files(download_path, filename):
def _check_similar_files(dest, filename):
"""
Checks for similar files in the download path based on the given filename.

Args:
download_path (str): The path where files are downloaded.
dest (str): The path where files are downloaded.
filename (str): The filename to check for similar files.

Returns:
bool: True if similar files exist, False otherwise.
filename_similar_names: A list of similar filenames if they exist, empty list otherwise.
"""
# pattern = download_path + '/**/' + os.path.splitext(filename)[0]
# pattern = dest + '/**/' + os.path.splitext(filename)[0]
filename_base = os.path.splitext(filename)[0]
filename_pattern = os.path.join(download_path, "**", filename_base)
filename_pattern = os.path.join(dest, "**", filename_base)
filename_similar = glob.glob(filename_pattern, recursive=True)

if filename_similar:
Expand All @@ -150,7 +150,7 @@ def run_module():
search_query=dict(type='str', required=False, default=''),
download_link=dict(type='str', required=False, default=''),
download_filename=dict(type='str', required=False, default=''),
download_path=dict(type='str', required=True),
dest=dict(type='str', required=True),
dry_run=dict(type='bool', required=False, default=False),
deduplicate=dict(type='str', required=False, default=''),
search_alternatives=dict(type='bool', required=False, default=False)
Expand All @@ -174,7 +174,7 @@ def run_module():
else:
query = None

download_path = module.params['download_path']
dest = module.params['dest']
download_link= module.params.get('download_link')
download_filename= module.params.get('download_filename')
dry_run = module.params.get('dry_run')
Expand Down Expand Up @@ -207,7 +207,7 @@ def run_module():

# Search for existing files using exact filename
filename = query if query else download_filename
filename_exact = os.path.join(download_path, filename)
filename_exact = os.path.join(dest, filename)
result['filename'] = filename

if os.path.exists(filename_exact):
Expand All @@ -216,7 +216,7 @@ def run_module():
module.exit_json(**result)
else:
# Exact file not found, search for similar files with pattern
filename_similar_exists, filename_similar_names = _check_similar_files(download_path, filename)
filename_similar_exists, filename_similar_names = _check_similar_files(dest, filename)
if filename_similar_exists and not (query and search_alternatives):
result['skipped'] = True
result['msg'] = f"Similar file(s) already exist: {', '.join(filename_similar_names)}"
Expand All @@ -235,14 +235,14 @@ def run_module():
result['filename'] = download_filename
result['alternative'] = True

filename_alternative_exact = os.path.join(download_path, download_filename)
filename_alternative_exact = os.path.join(dest, download_filename)
if os.path.exists(filename_alternative_exact):
result['skipped'] = True
result['msg'] = f"Alternative file already exists: {download_filename} - original file {query} is not available to download"
module.exit_json(**result)
else:
# Exact file not found, search for similar files with pattern
filename_similar_exists, filename_similar_names = _check_similar_files(download_path, download_filename)
filename_similar_exists, filename_similar_names = _check_similar_files(dest, download_filename)
if filename_similar_exists:
result['skipped'] = True
result['msg'] = f"Similar alternative file(s) already exist: {', '.join(filename_similar_names)}"
Expand All @@ -252,13 +252,13 @@ def run_module():
elif filename != download_filename and not alternative_found:
result['filename'] = download_filename

if os.path.exists(os.path.join(download_path, download_filename)):
if os.path.exists(os.path.join(dest, download_filename)):
result['skipped'] = True
result['msg'] = f"File already exists with correct name: {download_filename}"
module.exit_json(**result)
else:
# Exact file not found, search for similar files with pattern
filename_similar_exists, filename_similar_names = _check_similar_files(download_path, download_filename)
filename_similar_exists, filename_similar_names = _check_similar_files(dest, download_filename)
if filename_similar_exists:
result['skipped'] = True
result['msg'] = f"Similar file(s) already exist for correct name {download_filename}: {', '.join(filename_similar_names)}"
Expand All @@ -281,7 +281,7 @@ def run_module():
else:
module.fail_json(msg="Download link {} is not available".format(download_link))

download_software(download_link, download_filename, download_path)
download_software(download_link, download_filename, dest)

# Update final results json
result['changed'] = True
Expand Down
Empty file removed roles/.gitkeep
Empty file.
27 changes: 27 additions & 0 deletions roles/sap_software_download/.yamllint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SPDX-License-Identifier: Apache-2.0
---
# Based on ansible-lint config
extends: default

rules:
braces: {max-spaces-inside: 1, level: error}
brackets: {max-spaces-inside: 1, level: error}
# colons: {max-spaces-after: -1, level: error}
# commas: {max-spaces-after: -1, level: error}
comments:
require-starting-space: false
min-spaces-from-content: 1
comments-indentation: disable
# document-start: disable
# empty-lines: {max: 3, level: error}
# hyphens: {level: error}
# indentation: disable
# key-duplicates: enable
line-length: disable
# new-line-at-end-of-file: disable
# new-lines: {type: unix}
# trailing-spaces: disable
truthy: disable
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
Loading