github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_cross_model_relations.py (about) 1 #!/usr/bin/env python 2 """Functional tests for Cross Model Relation (CMR) functionality. 3 4 This test exercises the CMR Juju functionality which allows applications to 5 communicate between different models (including across controllers/clouds). 6 7 The outline of this feature can be found here[1]. 8 9 This test will exercise the following aspects: 10 - Ensure a user is able to create an offer of an applications' endpoint 11 including: 12 - A user is able to consume and relate to the offer 13 - Workload data successfully provided 14 - The offer appears in the list-offers output 15 - The user is able to name the offer 16 - The user is able to remove the offer 17 - Ensure an admin can grant a user access to an offer 18 - The consuming user finds the offer via 'find-offer' 19 20 The above feature tests will be run on: 21 - A single controller environment 22 - Multiple controllers where each controller is in a different cloud. 23 24 25 [1] https://docs.google.com/document/d/1IBTrqQSP3nrx5mTd_1vtUJ5YF28u9KJNTldUmUqrkJM/ # NOQA 26 """ 27 28 from __future__ import print_function 29 30 import argparse 31 import logging 32 import sys 33 import yaml 34 from textwrap import dedent 35 36 from deploy_stack import ( 37 BootstrapManager, 38 ) 39 from jujupy.client import ( 40 Controller, 41 register_user_interactively, 42 ) 43 from jujupy.models import ( 44 temporary_model 45 ) 46 from jujucharm import local_charm_path 47 from utility import ( 48 add_basic_testing_arguments, 49 configure_logging, 50 JujuAssertionError, 51 ) 52 53 54 __metaclass__ = type 55 56 57 log = logging.getLogger("assess_cross_model_relations") 58 59 60 def assess_cross_model_relations_single_controller(args): 61 """Assess that offers can be consumed in models on the same controller.""" 62 bs_manager = BootstrapManager.from_args(args) 63 with bs_manager.booted_context(args.upload_tools): 64 offer_model = bs_manager.client 65 with temporary_model(offer_model, 'consume-model') as consume_model: 66 ensure_cmr_offer_management(offer_model) 67 ensure_cmr_offer_consumption_and_relation( 68 offer_model, consume_model) 69 70 71 def assess_cross_model_relations_multiple_controllers(args): 72 """Offers must be able to consume models on different controllers.""" 73 consume_bs_args = extract_second_provider_details(args) 74 consume_bs_manager = BootstrapManager.from_args(consume_bs_args) 75 76 offer_bs_manager = BootstrapManager.from_args(args) 77 with offer_bs_manager.booted_context(args.upload_tools): 78 offer_model = offer_bs_manager.client 79 with consume_bs_manager.booted_context(consume_bs_args.upload_tools): 80 consume_model = consume_bs_manager.client 81 ensure_user_can_consume_offer(offer_model, consume_model) 82 83 84 def ensure_cmr_offer_management(client): 85 """Ensure creation, listing and deletion of offers work. 86 87 Deploy dummy-source application onto `client` and offer it's endpoint. 88 Ensure that: 89 - The offer attempt is successful 90 - The offer is shown in 'list-offers' 91 - The offer can be deleted (and no longer appear in 'list-offers') 92 93 :param client: ModelClient used to create a new model and attempt 'offer' 94 commands on 95 """ 96 with temporary_model(client, 'offer-management') as management_model: 97 app_name = 'dummy-source' 98 99 deploy_local_charm(management_model, app_name) 100 101 offer_url = assert_offer_is_listed( 102 management_model, app_name, offer_name='kitchen-sink') 103 assert_offer_can_be_deleted(management_model, offer_url) 104 105 offer_url = assert_offer_is_listed(management_model, app_name) 106 assert_offer_can_be_deleted(management_model, offer_url) 107 108 109 def ensure_cmr_offer_consumption_and_relation(offer_client, consume_client): 110 """Ensure offers can be consumed by another model. 111 112 :param offer_client: ModelClient model that will be the source of the 113 offer. 114 :param consume_client: ModelClient model that will consume the offered 115 application endpoint. 116 """ 117 with temporary_model(offer_client, 'relation-source') as source_client: 118 with temporary_model(consume_client, 'relation-sink') as sink_client: 119 offer_url, offer_name = deploy_and_offer_db_app(source_client) 120 assert_relating_to_offer_succeeds(sink_client, offer_url) 121 assert_saas_url_is_correct(sink_client, offer_name, offer_url) 122 123 124 def ensure_user_can_consume_offer(offer_client, consume_client): 125 """Ensure admin is able to grant a user access to an offer. 126 127 Almost the same as `ensure_cmr_offer_consumption_and_relation` except in 128 this a user is created on all controllers (might be a single controller 129 or 2) with the permissions of 'login' for the source controller and 'write' 130 for the model into which the user will deploy an application to consume the 131 offer. 132 133 :param offer_client: ModelClient model that will be the source of the 134 offer. 135 :param consume_client: ModelClient model that will consume the offered 136 application endpoint. 137 """ 138 with temporary_model(offer_client, 'relation-source') as source_client: 139 with temporary_model(consume_client, 'relation-sink') as sink_client: 140 offer_url, offer_name = deploy_and_offer_db_app(source_client) 141 142 username = 'theundertaker' 143 token = source_client.add_user_perms(username) 144 user_sink_client = register_user_on_controller( 145 sink_client, username, token) 146 147 source_client.controller_juju( 148 'grant', 149 (username, 'consume', offer_url)) 150 151 offers_found = yaml.safe_load( 152 user_sink_client.get_juju_output( 153 'find-offers', 154 '--interface', 'mysql', 155 '--format', 'yaml', 156 include_e=False)) 157 # There must only be one offer 158 user_offer_url = offers_found.keys()[0] 159 160 assert_relating_to_offer_succeeds(sink_client, user_offer_url) 161 assert_saas_url_is_correct( 162 sink_client, offer_name, user_offer_url) 163 164 165 def assert_offer_is_listed(client, app_name, offer_name=None): 166 """Assert that an offered endpoint is listed. 167 168 :param client: ModelClient for model to use. 169 :param app_name: Name of the deployed application to make an offer for. 170 :param offer_name: If not None is used to name the endpoint offer. 171 :return: String URL of the resulting offered endpoint. 172 """ 173 log.info('Assessing {} offers.'.format( 174 'named' if offer_name else 'unnamed')) 175 176 expected_url, offer_key = offer_endpoint( 177 client, app_name, 'sink', offer_name=offer_name) 178 offer_output = yaml.safe_load( 179 client.get_juju_output('offers', '--format', 'yaml')) 180 181 fully_qualified_offer = '{controller}:{offer_url}'.format( 182 controller=client.env.controller.name, 183 offer_url=offer_output[offer_key]['offer-url']) 184 try: 185 if fully_qualified_offer != expected_url: 186 raise JujuAssertionError( 187 'Offer URL mismatch.\n{actual} != {expected}'.format( 188 actual=offer_output[offer_key]['offer-url'], 189 expected=expected_url)) 190 except KeyError: 191 raise JujuAssertionError('No offer URL found in offers output.') 192 193 log.info('PASS: Assert offer is listed.') 194 return expected_url 195 196 197 def assert_offer_can_be_deleted(client, offer_url): 198 """Assert that an offer can be successfully deleted.""" 199 client.juju('remove-offer', (offer_url), include_e=False) 200 offer_output = yaml.safe_load( 201 client.get_juju_output('offers', '--format', 'yaml')) 202 203 if offer_output != {}: 204 raise JujuAssertionError( 205 'Failed to remove offer "{}"'.format(offer_url)) 206 log.info('PASS: Assert offer is removed.') 207 208 209 def assert_relating_to_offer_succeeds(client, offer_url): 210 """Deploy mediawiki on client and relate to provided `offer_url`. 211 212 Raises an exception if the workload status does not move to 'active' within 213 the default timeout (600 seconds). 214 """ 215 client.deploy('cs:mediawiki') 216 # No need to check workloads until the relation is set. 217 client.wait_for_started() 218 # mediawiki workload is blocked ('Database needed') until a db 219 # relation is successfully made. 220 client.juju('relate', ('mediawiki:db', offer_url)) 221 client.wait_for_workloads() 222 223 224 def assert_saas_url_is_correct(client, offer_name, offer_url): 225 """Offer url of Saas status field must match the expected `offer_url`.""" 226 status_saas_check = client.get_status() 227 status_saas_url = status_saas_check.status[ 228 'application-endpoints'][offer_name]['url'] 229 if status_saas_url != offer_url: 230 raise JujuAssertionError( 231 'Consuming models status does not state status of the' 232 'consumed offer.') 233 234 235 def deploy_and_offer_db_app(client): 236 """Deploy mysql application and offer it's db endpoint. 237 238 :return: tuple of (resulting offer url, offer name) 239 """ 240 client.deploy('cs:mysql') 241 client.wait_for_started() 242 client.wait_for_workloads() 243 return offer_endpoint(client, 'mysql', 'db') 244 245 246 def offer_endpoint(client, app_name, relation_name, offer_name=None): 247 """Create an endpoint offer for `app_name` with optional name. 248 249 :param client: ModelClient of model to operate on. 250 :param app_name: Deployed application name to create offer for. 251 :param offer_name: If not None create the offer with this name. 252 :return: Tuple of the resulting offer url (including controller) and the 253 offer name (default or named). 254 """ 255 model_name = client.env.environment 256 offer_endpoint = '{model}.{app}:{relation}'.format( 257 model=model_name, 258 app=app_name, 259 relation=relation_name) 260 offer_args = [offer_endpoint, '-c', client.env.controller.name] 261 if offer_name: 262 offer_args.append(offer_name) 263 client.juju('offer', tuple(offer_args), include_e=False) 264 265 offer_name = offer_name if offer_name else app_name 266 offer_url = '{controller}:{user}/{model}.{offer}'.format( 267 controller=client.env.controller.name, 268 user=client.env.user_name, 269 model=client.env.environment, 270 offer=offer_name) 271 return offer_url, offer_name 272 273 274 def register_user_on_controller(client, username, token): 275 """Register user with `token` on `client`s controller. 276 277 :return: ModelClient object for the registered user. 278 """ 279 controller_name = 'cmr_test' 280 user_client = client.clone(env=client.env.clone()) 281 user_client.env.user_name = username 282 user_client.env.controller = Controller(controller_name) 283 register_user_interactively(user_client, token, controller_name) 284 return user_client 285 286 287 def deploy_local_charm(client, app_name): 288 charm_path = local_charm_path( 289 charm=app_name, juju_ver=client.version) 290 client.deploy(charm_path) 291 client.wait_for_started() 292 293 294 def extract_second_provider_details(args): 295 """Create a Namespace suitable for use with BootstrapManager.from_args. 296 297 Using the 'secondary' environment details returns a argparse.Namespace 298 object that can be used with BootstrapManager.from_args to get a 299 bootstrap-able BootstrapManager. 300 """ 301 new_args = vars(args).copy() 302 new_args['env'] = new_args['secondary_env'] 303 new_args['region'] = new_args.get('secondary-region') 304 new_args['temp_env_name'] = '{}-secondary'.format( 305 new_args['temp_env_name']) 306 return argparse.Namespace(**new_args) 307 308 309 def parse_args(argv): 310 parser = argparse.ArgumentParser( 311 description="Cross Model Relations functional test.") 312 parser.add_argument( 313 '--secondary-env', 314 help=dedent("""\ 315 The second provider to use for the test. 316 If set the test will assess CMR functionality between the provider 317 set in `primary-env` and this env (`secondary-env`). 318 """)) 319 parser.add_argument( 320 '--secondary-region', 321 help='Override the default region for the secondary environment.') 322 add_basic_testing_arguments(parser) 323 return parser.parse_args(argv) 324 325 326 def main(argv=None): 327 args = parse_args(argv) 328 configure_logging(args.verbose) 329 330 assess_cross_model_relations_single_controller(args) 331 332 if args.secondary_env: 333 log.info('Assessing multiple controllers.') 334 assess_cross_model_relations_multiple_controllers(args) 335 336 return 0 337 338 339 if __name__ == '__main__': 340 sys.exit(main())