github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_agent_metadata.py (about) 1 #!/usr/bin/env python 2 """ 3 Juju agent-metadata-url validation on passing as an argument during 4 juju bootstrap 5 6 Juju agent-metadata-url validation using cloud definition yaml file to 7 verify that agent-metadata-url specified in the yaml file is applied 8 while running juju boostrap command. 9 10 Usage: python assess_agent_metadata.py --agent-file=/path/to/juju-*.tgz 11 12 Example: python assess_agent_metadata.py 13 --agent-file=/home/juju/juju-2.0.1-xenial-amd64.tgz 14 """ 15 16 from __future__ import print_function 17 18 from argparse import ArgumentParser 19 from contextlib import contextmanager 20 from hashlib import sha256 21 22 import logging 23 import os 24 import subprocess 25 import sys 26 import json 27 28 from deploy_stack import ( 29 BootstrapManager, 30 ) 31 32 from utility import ( 33 add_basic_testing_arguments, 34 configure_logging, 35 JujuAssertionError, 36 temp_dir, 37 temp_yaml_file, 38 ) 39 from remote import ( 40 remote_from_unit, 41 remote_from_address, 42 ) 43 from jujucharm import ( 44 local_charm_path, 45 ) 46 47 log = logging.getLogger("assess_agent_metadata") 48 49 50 def get_sha256_sum(filename): 51 """ 52 Get SHA256 sum of the given filename 53 :param filename: A string representing the filename to operate on 54 """ 55 with open(filename, 'rb') as infile: 56 return sha256(infile.read()).hexdigest() 57 58 59 def assert_cloud_details_are_correct(client, cloud_name, example_cloud): 60 """ 61 Check juju add-cloud is performed successfully and it is available 62 in the juju client. 63 :param client: The juju client 64 :param cloud_name: The name of the cloud added 65 :param example_cloud: The content of the cloud 66 """ 67 clouds = client.env.read_clouds() 68 try: 69 if clouds['clouds'][cloud_name] != example_cloud: 70 raise JujuAssertionError('Cloud mismatch') 71 except KeyError: 72 raise JujuAssertionError('Cloud missing {}'.format(cloud_name)) 73 74 75 def get_local_url_and_sha256(agent_dir, controller_url, agent_stream): 76 """ 77 Get the agent URL (local file location: file:///) and SHA256 78 of the agent-file passed 79 :param agent_dir: The top level directory location of agent file. 80 :param controller_url: The controller used agent file url 81 :param agent_stream: String representing agent stream name 82 """ 83 local_url = os.path.join(agent_dir, "tools", agent_stream, 84 os.path.basename(controller_url)) 85 86 local_sha256 = get_sha256_sum(local_url) 87 local_file_path = "file://" + local_url 88 return local_file_path, local_sha256 89 90 91 def get_controller_url_and_sha256(client): 92 """ 93 Get the agent url and sha256 of the launched client 94 :param client: Juju client 95 """ 96 controller_client = client.get_controller_client() 97 output = controller_client.run( 98 ['cat /var/lib/juju/tools/machine-0/downloaded-tools.txt'], 99 machines=['0']) 100 stdout_details = json.loads(output[0]['Stdout']) 101 return stdout_details['url'], stdout_details['sha256'] 102 103 104 def assert_metadata_is_correct(expected_agent_metadata_url, client): 105 """ 106 verify the client agent-metadata-url matches the specified value 107 :param expected_agent_metadata_url: The expected agent file path. 108 :param client: Juju client 109 """ 110 data = client.get_model_config() 111 actual_agent_metadata_url = data['agent-metadata-url']['value'] 112 if expected_agent_metadata_url != actual_agent_metadata_url: 113 raise JujuAssertionError( 114 'agent-metadata-url mismatch. Expected: {} Got: {}'.format( 115 expected_agent_metadata_url, actual_agent_metadata_url)) 116 117 log.info('bootstrap successfully with agent-metadata-url={}'.format( 118 actual_agent_metadata_url)) 119 120 121 def verify_deployed_charm(client, remote, unit="0"): 122 """Verify the deployed charm 123 124 Make sure deployed charm uses the same juju tool of the 125 controller by verifying the sha256 sum 126 127 :param client: Juju client 128 :param remote: The remote object of the deployed charm 129 :param unit: String representation of deployed charm unit. 130 """ 131 output = remote.cat( 132 "/var/lib/juju/tools/machine-{}/downloaded-tools.txt".format(unit)) 133 134 deserialized_output = json.loads(output) 135 _, controller_sha256 = get_controller_url_and_sha256(client) 136 137 if deserialized_output['sha256'] != controller_sha256: 138 raise JujuAssertionError( 139 'agent-metadata-url mismatch. Expected: {} Got: {}'.format( 140 controller_sha256, deserialized_output)) 141 142 log.info("Charm verification done successfully") 143 144 145 def deploy_machine_and_verify(client, series="bionic"): 146 """Deploy machine using juju add-machine of specified series 147 and verify that it make use of same agent-file used by the 148 controller. 149 150 :param client: Juju client 151 :param series: The charm series to deploy 152 """ 153 old_status = client.get_status() 154 client.juju('add-machine', ('--series', series)) 155 new_status = client.wait_for_started() 156 157 # This will iterate only once because we just added only one 158 # machine after old_status to new_status. 159 for unit, machine in new_status.iter_new_machines(old_status): 160 hostname = machine.get('dns-name') 161 if hostname: 162 remote = remote_from_address(hostname, machine.get('series')) 163 verify_deployed_charm(client, remote, unit) 164 else: 165 raise JujuAssertionError( 166 'Unable to get information about added machine') 167 168 log.info("add-machine verification done successfully") 169 170 171 def deploy_charm_and_verify(client, series="bionic", charm_app="dummy-source"): 172 """ 173 Deploy dummy charm from local repository and 174 verify it uses the specified agent-metadata-url option 175 :param client: Juju client 176 :param series: The charm series to deploy 177 :param charm_app: Juju charm application 178 """ 179 charm_source = local_charm_path( 180 charm=charm_app, juju_ver=client.version, series=series) 181 client.deploy(charm_source) 182 client.wait_for_started() 183 client.set_config(charm_app, {'token': 'one'}) 184 client.wait_for_workloads() 185 remote = remote_from_unit(client, "{}/0".format(charm_app)) 186 verify_deployed_charm(client, remote) 187 log.info( 188 "Successfully deployed charm {} of series {} and verified".format( 189 "dummy-source", series)) 190 191 192 def verify_deployed_tool(agent_dir, client, agent_stream): 193 """ 194 Verify the bootstrapped controller makes use of the the specified 195 agent-metadata-url. 196 :param agent_dir: The top level directory location of agent file. 197 :param client: Juju client 198 :param agent_stream: String representing agent stream name 199 """ 200 controller_url, controller_sha256 = get_controller_url_and_sha256(client) 201 202 local_url, local_sha256 = get_local_url_and_sha256( 203 agent_dir, controller_url, agent_stream) 204 205 if local_url != controller_url: 206 raise JujuAssertionError( 207 "mismatch local URL {} and controller URL {}".format( 208 local_url, controller_url)) 209 210 if local_sha256 != controller_sha256: 211 raise JujuAssertionError( 212 "mismatch local SHA256 {} and controller SHA256 {}".format( 213 local_sha256, controller_sha256)) 214 215 216 def set_new_log_dir(bs_manager, dir_name): 217 log_dir = os.path.join(bs_manager.log_dir, dir_name) 218 os.mkdir(log_dir) 219 bs_manager.log_dir = log_dir 220 221 222 def get_controller_series_and_alternative_series(client): 223 """Get controller and alternative controller series 224 225 Returns the series used by the controller and an alternative series 226 that is not used by the controller. 227 228 :param client: The juju client 229 :return: controller and non-controller series 230 """ 231 supported_series = ['bionic', 'xenial', 'trusty', 'zesty'] 232 controller_status = client.get_status(controller=True) 233 machines = dict(controller_status.iter_machines()) 234 controller_series = machines['0']['series'] 235 try: 236 supported_series.remove(controller_series) 237 except: 238 raise ValueError("Unknown series {}".format(controller_series)) 239 return controller_series, supported_series[0] 240 241 242 def assess_metadata(args, agent_dir, agent_stream): 243 """ 244 Bootstrap juju controller with agent-metadata-url value 245 and verify that bootstrapped controller makes use of specified 246 agent-metadata-url value. 247 :param args: Parsed command line arguments 248 :param agent_dir: The top level directory location of agent file. 249 :param agent_stream: String representing agent stream name 250 """ 251 bs_manager = BootstrapManager.from_args(args) 252 client = bs_manager.client 253 agent_metadata_url = os.path.join(agent_dir, "tools") 254 255 client.env.discard_option('tools-metadata-url') 256 client.env.update_config( 257 { 258 'agent-metadata-url': agent_metadata_url, 259 'agent-stream': agent_stream 260 } 261 ) 262 log.info('bootstrap to use --agent_metadata_url={}'.format( 263 agent_metadata_url)) 264 client.generate_tool(agent_dir, agent_stream) 265 set_new_log_dir(bs_manager, "assess_metadata") 266 267 with bs_manager.booted_context(args.upload_tools): 268 log.info('Metadata bootstrap successful.') 269 assert_metadata_is_correct(agent_metadata_url, client) 270 verify_deployed_tool(agent_dir, client, agent_stream) 271 log.info("Successfully deployed and verified agent-metadata-url") 272 series_details = get_controller_series_and_alternative_series(client) 273 controller_series, alt_controller_series = series_details 274 deploy_charm_and_verify(client, controller_series, "dummy-source") 275 deploy_machine_and_verify(client, alt_controller_series) 276 277 278 def get_cloud_details(client, agent_metadata_url, agent_stream): 279 """ 280 Create a cloud detail content to be used during add-cloud. 281 :param client: Juju client 282 :param agent_metadata_url: Sting representing agent-metadata-url 283 :param agent_stream: String representing agent stream name 284 """ 285 cloud_name = client.env.get_cloud() 286 cloud_details = { 287 'clouds': { 288 cloud_name: { 289 'type': client.env.provider, 290 'regions': {client.env.get_region(): {}}, 291 'config': { 292 'agent-metadata-url': 'file://{}'.format( 293 agent_metadata_url), 294 'agent-stream': agent_stream, 295 } 296 } 297 } 298 } 299 return cloud_details 300 301 302 def assess_add_cloud(args, agent_dir, agent_stream): 303 """ 304 Perform juju add-cloud by creating a yaml file for cloud 305 with agent-metadata-url value and bootstrap the juju environment. 306 :param args: Parsed command line arguments 307 :param agent_dir: The top level directory location of agent file. 308 :param agent_stream: String representing agent stream name 309 """ 310 311 bs_manager = BootstrapManager.from_args(args) 312 client = bs_manager.client 313 agent_metadata_url = os.path.join(agent_dir, "tools") 314 # Remove the tool metadata url from the config (note the name, is 315 # for historic reasons) 316 client.env.discard_option('tools-metadata-url') 317 cloud_details = get_cloud_details(client, agent_metadata_url, agent_stream) 318 319 with temp_yaml_file(cloud_details) as new_cloud: 320 cloud_name = client.env.get_cloud() 321 client.add_cloud(cloud_name, new_cloud) 322 # Need to make sure we've refreshed any cache that we might have (as 323 # this gets written to file during the bootstrap process. 324 client.env.load_yaml() 325 clouds = cloud_details['clouds'][cloud_name] 326 assert_cloud_details_are_correct(client, cloud_name, clouds) 327 328 client.generate_tool(agent_dir, agent_stream) 329 set_new_log_dir(bs_manager, "assess_add_cloud") 330 331 with bs_manager.booted_context(args.upload_tools): 332 log.info('Metadata bootstrap successful.') 333 verify_deployed_tool(agent_dir, client, agent_stream) 334 log.info("Successfully deployed and verified add-cloud") 335 deploy_charm_and_verify(client, "bionic", "dummy-source") 336 log.info("Successfully deployed charm and verified") 337 338 339 def clone_tgz_file_and_change_shasum(original_tgz_file, new_tgz_file): 340 """ 341 Create a new tgz file from the original tgz file and then add empty file 342 to it so that the sha256 sum of the new tgz file will be different from 343 that of original tgz file. We use this to make sure that controller 344 deployed on bootstrap used of the new tgz file. 345 :param original_tgz_file: The source tgz file 346 :param new_tgz_file: The destination tgz file 347 """ 348 if not original_tgz_file.endswith(".tgz"): 349 raise Exception("{} is not tgz file".format(original_tgz_file)) 350 try: 351 command = "cat {} <(echo -n ''| gzip)> {}".format( 352 original_tgz_file, new_tgz_file) 353 subprocess.Popen(command, shell=True, executable='/bin/bash') 354 except subprocess.CalledProcessError as e: 355 raise Exception("Failed to create a tool file {} - {}".format( 356 original_tgz_file, e)) 357 358 359 @contextmanager 360 def make_unique_tool(agent_file, agent_stream): 361 """ 362 Create a tool directory for juju agent tools and stream and invoke 363 clone_tgz_file_and_change_shasum function for the agent-file passed. 364 :param agent_file: The agent file to use 365 :param agent_stream: String representing agent stream name 366 """ 367 with temp_dir() as agent_dir: 368 juju_tool_src = os.path.join(agent_dir, "tools", agent_stream) 369 os.makedirs(juju_tool_src) 370 clone_tgz_file_and_change_shasum(agent_file, os.path.join( 371 juju_tool_src, os.path.basename(agent_file))) 372 log.debug("Created agent directory to perform bootstrap".format( 373 agent_dir)) 374 yield agent_dir 375 376 377 def parse_args(argv): 378 """Parse all arguments.""" 379 parser = ArgumentParser( 380 description="Test bootstrap for agent-metdadata-url") 381 382 add_basic_testing_arguments(parser, existing=False) 383 384 parser.add_argument('--agent-file', required=True, action='store', 385 help='agent file to be used during bootstrap.') 386 387 return parser.parse_args(argv) 388 389 390 def main(argv=None): 391 args = parse_args(argv) 392 configure_logging(args.verbose) 393 394 if not os.path.isfile(args.agent_file): 395 raise Exception( 396 "Unable to find juju agent file {}".format(args.agent_file)) 397 398 agent_stream = args.agent_stream if args.agent_stream else 'testing' 399 400 with make_unique_tool(args.agent_file, agent_stream) as agent_dir: 401 assess_metadata(args, agent_dir, agent_stream) 402 403 with make_unique_tool(args.agent_file, agent_stream) as agent_dir: 404 assess_add_cloud(args, agent_dir, agent_stream) 405 406 return 0 407 408 409 if __name__ == '__main__': 410 sys.exit(main())