4
4
5
5
6
6
import functools
7
+ import json
8
+ import logging
9
+ import os
7
10
import platform
8
- import unittest
11
+ import re
12
+ import shutil
9
13
import string
10
14
import subprocess
15
+ import tempfile
16
+ import unittest
17
+
18
+ import boto3
19
+ from botocore .config import Config as BotoConfig
20
+ from botocore .exceptions import ClientError as BotoClientError
21
+
22
+
23
+ logger = logging .getLogger (__name__ )
24
+ logging .basicConfig (format = '%(asctime)s %(levelname)s: %(message)s' , level = logging .INFO )
11
25
12
26
13
27
def koji_command (* args , _input = None , _globals = None , ** kwargs ):
28
+ return koji_command_cwd (* args , _input = _input , _globals = _globals , ** kwargs )
29
+
30
+
31
+ def koji_command_cwd (* args , cwd = None , _input = None , _globals = None , ** kwargs ):
14
32
args = list (args ) + [f'--{ k } ={ v } ' for k , v in kwargs .items ()]
15
33
if _globals :
16
34
args = [f'--{ k } ={ v } ' for k , v in _globals .items ()] + args
17
35
cmd = ["koji" ] + args
18
- print ( cmd )
36
+ logger . info ( "Running %s" , str ( cmd ) )
19
37
return subprocess .run (cmd ,
38
+ cwd = cwd ,
20
39
encoding = "utf-8" ,
21
40
stdout = subprocess .PIPE ,
22
41
stderr = subprocess .STDOUT ,
@@ -92,16 +111,28 @@ def testing_repos(self):
92
111
93
112
94
113
class TestIntegration (unittest .TestCase ):
114
+ logger = logging .getLogger (__name__ )
95
115
96
116
def setUp (self ):
97
- global_args = dict (
117
+ self . koji_global_args = dict (
98
118
server = "http://localhost:8080/kojihub" ,
119
+ topurl = "http://localhost:8080/kojifiles" ,
99
120
user = "kojiadmin" ,
100
121
password = "kojipass" ,
101
122
authtype = "password" )
102
123
self .koji = functools .partial (koji_command ,
103
124
"osbuild-image" ,
104
- _globals = global_args )
125
+ _globals = self .koji_global_args )
126
+
127
+ self .workdir = tempfile .mkdtemp ()
128
+ # EC2 image ID to clean up in tearDown() if set to a value
129
+ self .ec2_image_id = None
130
+
131
+ def tearDown (self ):
132
+ shutil .rmtree (self .workdir )
133
+ if self .ec2_image_id is not None :
134
+ self .delete_ec2_image (self .ec2_image_id )
135
+ self .ec2_image_id = None
105
136
106
137
def check_res (self , res : subprocess .CompletedProcess ):
107
138
if res .returncode != 0 :
@@ -117,6 +148,55 @@ def check_fail(self, res: subprocess.CompletedProcess):
117
148
"\n error: " + res .stdout )
118
149
self .fail (msg )
119
150
151
+ def task_id_from_res (self , res : subprocess .CompletedProcess ) -> str :
152
+ """
153
+ Extract the Task ID from `koji osbuild-image` command output and return it.
154
+ """
155
+ r = re .compile (r'^Created task:[ \t]+(\d+)$' , re .MULTILINE )
156
+ m = r .search (res .stdout )
157
+ if not m :
158
+ self .fail ("Could not find task id in output" )
159
+ return m .group (1 )
160
+
161
+ @staticmethod
162
+ def get_ec2_client ():
163
+ aws_region = os .getenv ("AWS_REGION" )
164
+ return boto3 .client ('ec2' , config = BotoConfig (region_name = aws_region ))
165
+
166
+ def check_ec2_image_exists (self , image_id : str ) -> None :
167
+ """
168
+ Check if an EC2 image with the given ID exists.
169
+ If not, fail the test case.
170
+ """
171
+ client = self .get_ec2_client ()
172
+ try :
173
+ resp = client .describe_images (ImageIds = [image_id ])
174
+ except BotoClientError as e :
175
+ self .fail (str (e ))
176
+ self .assertEqual (len (resp ["Images" ]), 1 )
177
+
178
+ def delete_ec2_image (self , image_id : str ) -> None :
179
+ client = self .get_ec2_client ()
180
+ # first get the snapshot ID associated with the image
181
+ try :
182
+ resp = client .describe_images (ImageIds = [image_id ])
183
+ except BotoClientError as e :
184
+ self .fail (str (e ))
185
+ self .assertEqual (len (resp ["Images" ]), 1 )
186
+
187
+ snapshot_id = resp ["Images" ][0 ]["BlockDeviceMappings" ][0 ]["Ebs" ]["SnapshotId" ]
188
+ # deregister the image
189
+ try :
190
+ resp = client .deregister_image (ImageId = image_id )
191
+ except BotoClientError as e :
192
+ self .logger .warning ("Failed to deregister image %s: %s" , image_id , str (e ))
193
+
194
+ # delete the associated snapshot
195
+ try :
196
+ resp = client .delete_snapshot (SnapshotId = snapshot_id )
197
+ except BotoClientError as e :
198
+ self .logger .warning ("Failed to delete snapshot %s: %s" , snapshot_id , str (e ))
199
+
120
200
def test_compose (self ):
121
201
"""Successful compose"""
122
202
# Simple test of a successful compose of RHEL
@@ -155,3 +235,67 @@ def test_unknown_tag_check(self):
155
235
"UNKNOWNTAG" ,
156
236
sut_info .os_arch )
157
237
self .check_fail (res )
238
+
239
+ def test_cloud_upload_aws (self ):
240
+ """Successful compose with cloud upload to AWS"""
241
+ sut_info = SutInfo ()
242
+
243
+ repos = []
244
+ for repo in sut_info .testing_repos ():
245
+ url = repo ["url" ]
246
+ package_sets = repo .get ("package_sets" )
247
+ repos += ["--repo" , url ]
248
+ if package_sets :
249
+ repos += ["--repo-package-sets" , package_sets ]
250
+
251
+ package = "aws"
252
+ aws_region = os .getenv ("AWS_REGION" )
253
+
254
+ upload_options = {
255
+ "region" : aws_region ,
256
+ "share_with_accounts" : [os .getenv ("AWS_API_TEST_SHARE_ACCOUNT" )]
257
+ }
258
+
259
+ upload_options_file = os .path .join (self .workdir , "upload_options.json" )
260
+ with open (upload_options_file , "w" , encoding = "utf-8" ) as f :
261
+ json .dump (upload_options , f )
262
+
263
+ res = self .koji (package ,
264
+ sut_info .os_version_major ,
265
+ sut_info .composer_distro_name ,
266
+ sut_info .koji_tag ,
267
+ sut_info .os_arch ,
268
+ "--wait" ,
269
+ * repos ,
270
+ f"--image-type={ package } " ,
271
+ f"--upload-options={ upload_options_file } " )
272
+ self .check_res (res )
273
+
274
+ task_id = self .task_id_from_res (res )
275
+ # Download files uploaded by osbuild plugins to the Koji build task.
276
+ # requires koji client of version >= 1.29.1
277
+ res_download = koji_command_cwd (
278
+ "download-task" , "--all" , task_id , cwd = self .workdir , _globals = self .koji_global_args
279
+ )
280
+ self .check_res (res_download )
281
+
282
+ # Extract information about the uploaded AMI from compose status response.
283
+ compose_status_file = os .path .join (self .workdir , "compose-status.noarch.json" )
284
+ with open (compose_status_file , "r" , encoding = "utf-8" ) as f :
285
+ compose_status = json .load (f )
286
+
287
+ self .assertEqual (compose_status ["status" ], "success" )
288
+ image_statuses = compose_status ["image_statuses" ]
289
+ self .assertEqual (len (image_statuses ), 1 )
290
+
291
+ upload_status = image_statuses [0 ]["upload_status" ]
292
+ self .assertEqual (upload_status ["status" ], "success" )
293
+ self .assertEqual (upload_status ["type" ], "aws" )
294
+
295
+ upload_options = upload_status ["options" ]
296
+ self .assertEqual (upload_options ["region" ], aws_region )
297
+
298
+ image_id = upload_options ["ami" ]
299
+ self .assertNotEqual (len (image_id ), 0 )
300
+ self .ec2_image_id = image_id
301
+ self .check_ec2_image_exists (image_id )
0 commit comments