3
from argparse import ArgumentParser
4
from contextlib import contextmanager
6
from textwrap import dedent
7
from subprocess import CalledProcessError
10
from jujucharm import (
19
from deploy_stack import (
24
from jujuci import add_credential_args
30
def prepare_dummy_env(client):
31
"""Use a client to prepare a dummy environment."""
32
charm_source = local_charm_path(
33
charm='dummy-source', juju_ver=client.version)
34
client.deploy(charm_source)
35
charm_sink = local_charm_path(charm='dummy-sink', juju_ver=client.version)
36
client.deploy(charm_sink)
37
token = get_random_string()
38
client.set_config('dummy-source', {'token': token})
39
client.juju('add-relation', ('dummy-source', 'dummy-sink'))
40
client.juju('expose', ('dummy-sink',))
44
def get_clients(initial, other, base_env, debug, agent_url):
45
"""Return the clients to use for testing."""
47
from tests.test_jujupy import fake_juju_client
48
environment = SimpleEnvironment.from_config(base_env)
49
client = fake_juju_client(env=environment)
50
return client, client, client
52
initial_client = client_from_config(base_env, initial, debug=debug)
53
environment = initial_client.env
55
environment.config.pop('tools-metadata-url', None)
56
other_client = initial_client.clone_path_cls(other)
57
# System juju is assumed to be released and the best choice for tearing
58
# down environments reliably. (For example, 1.18.x cannot tear down
59
# environments with alpha agent-versions.)
60
released_client = initial_client.clone_path_cls(None)
61
# If released_client is a different major version, it cannot tear down
62
# initial client, so use initial client for teardown.
64
isinstance(released_client, EnvJujuClient1X) !=
65
isinstance(initial_client, EnvJujuClient1X)
67
released_client = initial_client
69
# If system juju is used, ensure it has identical env to
71
released_client.env = initial_client.env
72
return initial_client, other_client, released_client
75
def assess_heterogeneous(initial, other, base_env, environment_name, log_dir,
76
upload_tools, debug, agent_url, agent_stream, series):
77
"""Top level function that prepares the clients and environment.
79
initial and other are paths to the binary used initially, and a binary
80
used later. base_env is the name of the environment to base the
81
environment on and environment_name is the new name for the environment.
83
initial_client, other_client, teardown_client = get_clients(
84
initial, other, base_env, debug, agent_url)
85
jes_enabled = initial_client.is_jes_enabled()
86
bs_manager = BootstrapManager(
87
environment_name, initial_client, teardown_client,
88
bootstrap_host=None, machines=[], series=series, agent_url=agent_url,
89
agent_stream=agent_stream, region=None, log_dir=log_dir,
90
keep_env=False, permanent=jes_enabled, jes_enabled=jes_enabled)
91
test_control_heterogeneous(bs_manager, other_client, upload_tools)
95
def run_context(bs_manager, other, upload_tools):
97
bs_manager.keep_env = True
98
with bs_manager.booted_context(upload_tools):
99
if other.env.juju_home != bs_manager.client.env.juju_home:
100
raise AssertionError('Juju home out of sync')
102
# Test clean shutdown of an environment.
103
callback_with_fallback(other, bs_manager.tear_down_client,
106
bs_manager.tear_down()
110
def test_control_heterogeneous(bs_manager, other, upload_tools):
111
"""Test if one binary can control an environment set up by the other."""
112
initial = bs_manager.client
113
released = bs_manager.tear_down_client
114
with run_context(bs_manager, other, upload_tools):
115
token = prepare_dummy_env(initial)
116
initial.wait_for_started()
117
if sys.platform != "win32":
118
# Currently, juju ssh is not working on Windows.
119
check_token(initial, token)
121
other.juju('run', ('--all', 'uname -a'))
122
other.get_config('dummy-source')
123
other.get_model_config()
124
other.juju('remove-relation', ('dummy-source', 'dummy-sink'))
125
status = other.get_status()
126
other.juju('unexpose', ('dummy-sink',))
127
status = other.get_status()
128
if status.get_applications()['dummy-sink']['exposed']:
129
raise AssertionError('dummy-sink is still exposed')
130
status = other.get_status()
131
charm_path = local_charm_path(
132
charm='dummy-sink', juju_ver=other.version)
133
juju_with_fallback(other, released, 'deploy',
134
(charm_path, 'sink2'))
135
other.wait_for_started()
136
other.juju('add-relation', ('dummy-source', 'sink2'))
137
status = other.get_status()
138
other.juju('expose', ('sink2',))
139
status = other.get_status()
140
if 'sink2' not in status.get_applications():
141
raise AssertionError('Sink2 missing')
142
other.remove_service('sink2')
143
for ignored in until_timeout(30):
144
status = other.get_status()
145
if 'sink2' not in status.get_applications():
148
raise AssertionError('Sink2 not destroyed')
149
other.juju('add-relation', ('dummy-source', 'dummy-sink'))
150
status = other.get_status()
151
relations = status.get_applications()['dummy-sink']['relations']
152
if not relations['source'] == ['dummy-source']:
153
raise AssertionError('source is not dummy-source.')
154
other.juju('expose', ('dummy-sink',))
155
status = other.get_status()
156
if not status.get_applications()['dummy-sink']['exposed']:
157
raise AssertionError('dummy-sink is not exposed')
158
other.juju('add-unit', ('dummy-sink',))
159
if not has_agent(other, 'dummy-sink/1'):
160
raise AssertionError('dummy-sink/1 was not added.')
161
other.juju('remove-unit', ('dummy-sink/1',))
162
status = other.get_status()
163
if has_agent(other, 'dummy-sink/1'):
164
raise AssertionError('dummy-sink/1 was not removed.')
165
container_type = other.preferred_container()
166
other.juju('add-machine', (container_type,))
167
status = other.get_status()
168
container_machine, = set(k for k, v in status.agent_items() if
169
k.endswith('/{}/0'.format(container_type)))
170
container_holder = container_machine.split('/')[0]
171
other.juju('remove-machine', (container_machine,))
172
wait_until_removed(other, container_machine)
173
other.juju('remove-machine', (container_holder,))
174
wait_until_removed(other, container_holder)
177
test_control_heterogeneous.__test__ = False
180
def juju_with_fallback(other, released, command, args, include_e=True):
181
"""Fallback to released juju when 1.18 fails.
183
Get as much test coverage of 1.18 as we can, by falling back to a released
184
juju for commands that we expect to fail (due to unsupported agent version
187
def call_juju(client):
188
client.juju(command, args, include_e=include_e)
189
return callback_with_fallback(other, released, call_juju)
192
def callback_with_fallback(other, released, callback):
193
for client in [other, released]:
196
except CalledProcessError:
197
if not client.version.startswith('1.18.'):
203
def nice_tear_down(client):
204
if client.is_jes_enabled():
205
client.kill_controller()
207
if client.destroy_environment(force=False) != 0:
208
raise CalledProcessError(1, 'juju destroy-environment')
211
def has_agent(client, agent_id):
212
return bool(agent_id in dict(client.get_status().agent_items()))
215
def wait_until_removed(client, agent_id):
216
"""Wait for an agent to be removed from the environment."""
217
for ignored in until_timeout(240):
218
if not has_agent(client, agent_id):
221
raise AssertionError('Machine not destroyed: {}.'.format(agent_id))
224
def check_series(client, machine='0', series=None):
225
"""Use 'juju ssh' to check that the deployed series meets expectations."""
226
result = client.get_juju_output('ssh', machine, 'lsb_release', '-c')
227
label, codename = result.rstrip().split('\t')
228
if label != 'Codename:':
229
raise AssertionError()
231
expected_codename = series
233
expected_codename = client.env.config['default-series']
234
if codename != expected_codename:
235
raise AssertionError(
236
'Series is {}, not {}'.format(codename, expected_codename))
239
def parse_args(argv=None):
240
parser = ArgumentParser(description=dedent("""\
241
Determine whether one juju version can control an environment created
244
parser.add_argument('initial', help='The initial juju binary.')
245
parser.add_argument('other', help='A different juju binary.')
246
parser.add_argument('base_environment', help='The environment to base on.')
247
parser.add_argument('environment_name', help='The new environment name.')
248
parser.add_argument('log_dir', help='The directory to dump logs to.')
250
'--upload-tools', action='store_true', default=False,
251
help='Upload local version of tools before bootstrapping.')
252
parser.add_argument('--debug', help='Run juju with --debug',
253
action='store_true', default=False)
254
parser.add_argument('--agent-url', default=None)
255
parser.add_argument('--agent-stream', action='store',
256
help='URL for retrieving agent binaries.')
257
parser.add_argument('--series', action='store',
258
help='Name of the Ubuntu series to use.')
259
add_credential_args(parser)
260
return parser.parse_args(argv)
265
configure_logging(logging.INFO)
266
assess_heterogeneous(args.initial, args.other, args.base_environment,
267
args.environment_name, args.log_dir,
268
args.upload_tools, args.debug, args.agent_url,
269
args.agent_stream, args.series)
272
if __name__ == '__main__':