delete_stale_ebs.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import os
  9. import requests
  10. import traceback
  11. import boto3
  12. import json
  13. from datetime import datetime
  14. from requests.auth import HTTPBasicAuth
  15. from urllib.parse import unquote
  16. class JenkinsAPIClient:
  17. def __init__(self, jenkins_base_url, jenkins_username, jenkins_api_token):
  18. self.jenkins_base_url = jenkins_base_url.rstrip('/')
  19. self.jenkins_username = jenkins_username
  20. self.jenkins_api_token = jenkins_api_token
  21. def get(self, url, retry=1):
  22. for i in range(retry):
  23. try:
  24. response = requests.get(url, auth=HTTPBasicAuth(self.jenkins_username, self.jenkins_api_token))
  25. if response.ok:
  26. return response.json()
  27. except Exception:
  28. traceback.print_exc()
  29. print(f'WARN: Get request {url} failed, retying....')
  30. print(f'WARN: Get request {url} failed, see exception for more details.')
  31. def get_pipeline(self, pipeline_name):
  32. url = f'{self.jenkins_base_url}/job/{pipeline_name}/api/json'
  33. # Use retry because Jenkins API call sometimes may fail when Jenkins server is on high load
  34. return self.get(url, retry=3)
  35. def get_branch_job(self, pipeline_name, branch_name):
  36. url = f'{self.jenkins_base_url}/blue/rest/organizations/jenkins/pipelines/{pipeline_name}/branches/{branch_name}'
  37. # Use retry because Jenkins API call sometimes may fail when Jenkins server is on high load
  38. return self.get(url, retry=3)
  39. def delete_branch_ebs_volumes(branch_name):
  40. """
  41. Make a fake branch deleted event and invoke the lambda function that we use to delete branch EBS volumes after a branch is deleted.
  42. """
  43. # Unescape branch name as it's URL encoded
  44. branch_name = unquote(branch_name)
  45. input = {
  46. "detail": {
  47. "event": "referenceDeleted",
  48. "repositoryName": "Lumberyard",
  49. "referenceName": branch_name
  50. }
  51. }
  52. client = boto3.client('lambda')
  53. # Invoke lambda function "AutoDeleteEBS-Lambda" asynchronously.
  54. # This lambda function can have 1000 concurrent runs.
  55. # we will setup a SQS/SNS queue to process the events if the event number exceeds the function capacity.
  56. client.invoke(
  57. FunctionName='AutoDeleteEBS-Lambda',
  58. InvocationType='Event',
  59. Payload=json.dumps(input),
  60. )
  61. def delete_old_branch_ebs_volumes(env):
  62. """
  63. Check last run time of each branch build, if it exceeds the retention days, delete the EBS volumes that are tied to the branch.
  64. """
  65. branch_volumes_deleted = []
  66. jenkins_client = JenkinsAPIClient(env['JENKINS_URL'], env['JENKINS_USERNAME'], env['JENKINS_API_TOKEN'])
  67. today_date = datetime.today().date()
  68. pipeline_name = env['PIPELINE_NAME']
  69. pipeline_job = jenkins_client.get_pipeline(pipeline_name)
  70. if not pipeline_job:
  71. print(f'ERROR: Cannot get data of pipeline job {pipeline_name}.')
  72. exit(1)
  73. branch_jobs = pipeline_job.get('jobs', [])
  74. retention_days = int(env['RETENTION_DAYS'])
  75. for branch_job in branch_jobs:
  76. branch_name = branch_job['name']
  77. branch_job = jenkins_client.get_branch_job(pipeline_name, branch_name)
  78. if not branch_job:
  79. print(f'WARN: Cannot get data of {branch_name} job , skipping branch {pipeline_name}.')
  80. continue
  81. latest_run = branch_job.get('latestRun')
  82. # If the job hasn't run, then there is no EBS volumes tied to that job
  83. if latest_run:
  84. latest_run_start_time = latest_run.get('startTime')
  85. latest_run_datetime = datetime.strptime(latest_run_start_time, '%Y-%m-%dT%H:%M:%S.%f%z')
  86. # Convert startTime to local timezone to compare, because Jenkins server may use a different timezone.
  87. latest_run_date = latest_run_datetime.astimezone().date()
  88. date_diff = today_date - latest_run_date
  89. if date_diff.days > retention_days:
  90. print(f'Branch {branch_name} job hasn\'t run for over {retention_days} days, deleting the EBS volumes of this branch...')
  91. delete_branch_ebs_volumes(branch_name)
  92. branch_volumes_deleted.append(branch_name)
  93. print('Deleted EBS volumes for branches:')
  94. print('\n'.join(branch_volumes_deleted))
  95. def get_required_env(env, keys):
  96. success = True
  97. for key in keys:
  98. try:
  99. env[key] = os.environ[key].strip()
  100. except KeyError:
  101. print(f'ERROR: {key} is not set in environment variable')
  102. success = False
  103. return success
  104. def main():
  105. env = {}
  106. required_env_list = [
  107. 'JENKINS_URL',
  108. 'JENKINS_USERNAME',
  109. 'JENKINS_API_TOKEN',
  110. 'PIPELINE_NAME',
  111. 'RETENTION_DAYS'
  112. ]
  113. if not get_required_env(env, required_env_list):
  114. print('ERROR: Required environment variable is not set, see log for more details.')
  115. delete_old_branch_ebs_volumes(env)
  116. if __name__ == "__main__":
  117. main()