github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/acceptancetests/assess_heterogeneous_control.py (about) 1 #!/usr/bin/env python3 2 3 from argparse import ArgumentParser 4 from contextlib import contextmanager 5 import logging 6 from textwrap import dedent 7 from subprocess import CalledProcessError 8 import sys 9 10 from utility import ( 11 configure_logging, 12 until_timeout, 13 ) 14 from jujuci import add_credential_args 15 from jujucharm import ( 16 local_charm_path, 17 ) 18 from deploy_stack import ( 19 BootstrapManager, 20 check_token, 21 get_random_string, 22 ) 23 24 from jujupy import ( 25 client_from_config, 26 fake_juju_client, 27 JujuData, 28 ) 29 30 31 def prepare_dummy_env(client): 32 """Use a client to prepare a dummy environment.""" 33 charm_source = local_charm_path( 34 charm='dummy-source', juju_ver=client.version) 35 client.deploy(charm_source) 36 charm_sink = local_charm_path(charm='dummy-sink', juju_ver=client.version) 37 client.deploy(charm_sink) 38 token = get_random_string() 39 client.set_config('dummy-source', {'token': token}) 40 client.juju('integrate', ('dummy-source', 'dummy-sink')) 41 client.juju('expose', ('dummy-sink',)) 42 return token 43 44 45 def get_clients(initial, other, base_env, debug, agent_url): 46 """Return the clients to use for testing.""" 47 if initial == 'FAKE': 48 environment = JujuData.from_config(base_env) 49 client = fake_juju_client(env=environment) 50 return client, client, client 51 52 initial_client = client_from_config(base_env, initial, debug=debug) 53 environment = initial_client.env 54 if agent_url is None: 55 environment.discard_option('tools-metadata-url') 56 other_client = initial_client.clone_from_path(other) 57 # This used to catch an exception of the config didn't match. 58 # version_client no longer exists so that no longer made sense. 59 released_client = initial_client.clone_from_path(None) 60 # If system juju is used, ensure it has identical env to 61 # initial_client. 62 released_client.env = initial_client.env 63 return initial_client, other_client, released_client 64 65 66 def assess_heterogeneous(initial, other, base_env, environment_name, log_dir, 67 upload_tools, debug, agent_url, agent_stream, series): 68 """Top level function that prepares the clients and environment. 69 70 initial and other are paths to the binary used initially, and a binary 71 used later. base_env is the name of the environment to base the 72 environment on and environment_name is the new name for the environment. 73 """ 74 initial_client, other_client, teardown_client = get_clients( 75 initial, other, base_env, debug, agent_url) 76 bs_manager = BootstrapManager( 77 environment_name, initial_client, teardown_client, 78 bootstrap_host=None, machines=[], series=series, arch=None, 79 agent_url=agent_url, agent_stream=agent_stream, region=None, 80 log_dir=log_dir, keep_env=False) 81 test_control_heterogeneous(bs_manager, other_client, upload_tools) 82 83 84 @contextmanager 85 def run_context(bs_manager, other, upload_tools): 86 try: 87 bs_manager.keep_env = True 88 with bs_manager.booted_context(upload_tools): 89 if other.env.juju_home != bs_manager.client.env.juju_home: 90 raise AssertionError('Juju home out of sync') 91 yield 92 # Test clean shutdown of an environment. 93 callback_with_fallback(other, bs_manager.tear_down_client, 94 nice_tear_down) 95 except Exception: 96 bs_manager.tear_down() 97 raise 98 99 100 def test_control_heterogeneous(bs_manager, other, upload_tools): 101 """Test if one binary can control an environment set up by the other.""" 102 initial = bs_manager.client 103 released = bs_manager.tear_down_client 104 with run_context(bs_manager, other, upload_tools): 105 token = prepare_dummy_env(initial) 106 initial.wait_for_started() 107 if sys.platform != "win32": 108 # Currently, juju ssh is not working on Windows. 109 check_token(initial, token) 110 check_series(other) 111 other.juju('exec', ('--all', 'uname -a')) 112 other.get_config('dummy-source') 113 other.get_model_config() 114 other.juju('remove-relation', ('dummy-source', 'dummy-sink')) 115 status = other.get_status() 116 other.juju('unexpose', ('dummy-sink',)) 117 status = other.get_status() 118 if status.get_applications()['dummy-sink']['exposed']: 119 raise AssertionError('dummy-sink is still exposed') 120 status = other.get_status() 121 charm_path = local_charm_path( 122 charm='dummy-sink', juju_ver=other.version) 123 juju_with_fallback(other, released, 'deploy', 124 (charm_path, 'sink2')) 125 other.wait_for_started() 126 other.juju('integrate', ('dummy-source', 'sink2')) 127 status = other.get_status() 128 other.juju('expose', ('sink2',)) 129 status = other.get_status() 130 if 'sink2' not in status.get_applications(): 131 raise AssertionError('Sink2 missing') 132 other.remove_application('sink2') 133 for ignored in until_timeout(30): 134 status = other.get_status() 135 if 'sink2' not in status.get_applications(): 136 break 137 else: 138 raise AssertionError('Sink2 not destroyed') 139 other.juju('integrate', ('dummy-source', 'dummy-sink')) 140 status = other.get_status() 141 relations = status.get_applications()['dummy-sink']['relations'] 142 if not relations['source'] == ['dummy-source']: 143 raise AssertionError('source is not dummy-source.') 144 other.juju('expose', ('dummy-sink',)) 145 status = other.get_status() 146 if not status.get_applications()['dummy-sink']['exposed']: 147 raise AssertionError('dummy-sink is not exposed') 148 other.juju('add-unit', ('dummy-sink',)) 149 if not has_agent(other, 'dummy-sink/1'): 150 raise AssertionError('dummy-sink/1 was not added.') 151 other.juju('remove-unit', ('dummy-sink/1',)) 152 status = other.get_status() 153 if has_agent(other, 'dummy-sink/1'): 154 raise AssertionError('dummy-sink/1 was not removed.') 155 container_type = other.preferred_container() 156 other.juju('add-machine', (container_type,)) 157 status = other.get_status() 158 container_machine, = set(k for k, v in status.agent_items() if 159 k.endswith('/{}/0'.format(container_type))) 160 container_holder = container_machine.split('/')[0] 161 other.remove_machine(container_machine) 162 wait_until_removed(other, container_machine) 163 other.remove_machine(container_holder) 164 wait_until_removed(other, container_holder) 165 166 167 # suppress nosetests 168 test_control_heterogeneous.__test__ = False 169 170 171 def juju_with_fallback(other, released, command, args, include_e=True): 172 """Fallback to released juju when 1.18 fails. 173 174 Get as much test coverage of 1.18 as we can, by falling back to a released 175 juju for commands that we expect to fail (due to unsupported agent version 176 format). 177 """ 178 def call_juju(client): 179 client.juju(command, args, include_e=include_e) 180 return callback_with_fallback(other, released, call_juju) 181 182 183 def callback_with_fallback(other, released, callback): 184 for client in [other, released]: 185 try: 186 callback(client) 187 except CalledProcessError: 188 if not client.version.startswith('1.18.'): 189 raise 190 else: 191 break 192 193 194 def nice_tear_down(client): 195 client.kill_controller() 196 197 198 def has_agent(client, agent_id): 199 return bool(agent_id in dict(client.get_status().agent_items())) 200 201 202 def wait_until_removed(client, agent_id): 203 """Wait for an agent to be removed from the environment.""" 204 for ignored in until_timeout(240): 205 if not has_agent(client, agent_id): 206 return 207 208 raise AssertionError('Machine not destroyed: {}.'.format(agent_id)) 209 210 211 def check_series(client, machine='0', series=None): 212 """Use 'juju ssh' to check that the deployed series meets expectations.""" 213 result = client.get_juju_output('ssh', machine, 'lsb_release', '-c') 214 label, codename = result.rstrip().split('\t') 215 if label != 'Codename:': 216 raise AssertionError() 217 if series: 218 expected_codename = series 219 else: 220 expected_codename = client.env.get_option('default-series') 221 if codename != expected_codename: 222 raise AssertionError( 223 'Series is {}, not {}'.format(codename, expected_codename)) 224 225 226 def parse_args(argv=None): 227 parser = ArgumentParser(description=dedent("""\ 228 Determine whether one juju version can control an environment created 229 by another version. 230 """)) 231 parser.add_argument('initial', help='The initial juju binary.') 232 parser.add_argument('other', help='A different juju binary.') 233 parser.add_argument('base_environment', help='The environment to base on.') 234 parser.add_argument('environment_name', help='The new environment name.') 235 parser.add_argument('log_dir', help='The directory to dump logs to.') 236 parser.add_argument( 237 '--upload-tools', action='store_true', default=False, 238 help='Upload local version of tools before bootstrapping.') 239 parser.add_argument('--debug', help='Run juju with --debug', 240 action='store_true', default=False) 241 parser.add_argument('--agent-url', default=None) 242 parser.add_argument('--agent-stream', action='store', 243 help='URL for retrieving agent binaries.') 244 parser.add_argument('--series', action='store', 245 help='Name of the Ubuntu series to use.') 246 add_credential_args(parser) 247 return parser.parse_args(argv) 248 249 250 def main(): 251 args = parse_args() 252 configure_logging(logging.INFO) 253 assess_heterogeneous(args.initial, args.other, args.base_environment, 254 args.environment_name, args.log_dir, 255 args.upload_tools, args.debug, args.agent_url, 256 args.agent_stream, args.series) 257 258 259 if __name__ == '__main__': 260 main()