~abentley/juju-ci-tools/client-from-config-4

« back to all changes in this revision

Viewing changes to assess_heterogeneous_control.py

  • Committer: Aaron Bentley
  • Date: 2014-02-24 17:18:29 UTC
  • mto: This revision was merged to the branch mainline in revision 252.
  • Revision ID: aaron.bentley@canonical.com-20140224171829-sz644yhoygu7m9dm
Use tags to identify and shut down instances.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
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 jujucharm import (
11
 
    local_charm_path,
12
 
)
13
 
from jujupy import (
14
 
    client_from_config,
15
 
    EnvJujuClient1X,
16
 
    SimpleEnvironment,
17
 
    until_timeout,
18
 
    )
19
 
from deploy_stack import (
20
 
    BootstrapManager,
21
 
    check_token,
22
 
    get_random_string,
23
 
    )
24
 
from jujuci import add_credential_args
25
 
from utility import (
26
 
    configure_logging,
27
 
)
28
 
 
29
 
 
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',))
41
 
    return token
42
 
 
43
 
 
44
 
def get_clients(initial, other, base_env, debug, agent_url):
45
 
    """Return the clients to use for testing."""
46
 
    if initial == 'FAKE':
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
51
 
    else:
52
 
        initial_client = client_from_config(base_env, initial, debug=debug)
53
 
        environment = initial_client.env
54
 
    if agent_url is None:
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.
63
 
    if (
64
 
            isinstance(released_client, EnvJujuClient1X) !=
65
 
            isinstance(initial_client, EnvJujuClient1X)
66
 
            ):
67
 
        released_client = initial_client
68
 
    else:
69
 
        # If system juju is used, ensure it has identical env to
70
 
        # initial_client.
71
 
        released_client.env = initial_client.env
72
 
    return initial_client, other_client, released_client
73
 
 
74
 
 
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.
78
 
 
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.
82
 
    """
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)
92
 
 
93
 
 
94
 
@contextmanager
95
 
def run_context(bs_manager, other, upload_tools):
96
 
    try:
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')
101
 
            yield
102
 
        # Test clean shutdown of an environment.
103
 
        callback_with_fallback(other, bs_manager.tear_down_client,
104
 
                               nice_tear_down)
105
 
    except:
106
 
        bs_manager.tear_down()
107
 
        raise
108
 
 
109
 
 
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)
120
 
            check_series(other)
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():
146
 
                break
147
 
        else:
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)
175
 
 
176
 
# suppress nosetests
177
 
test_control_heterogeneous.__test__ = False
178
 
 
179
 
 
180
 
def juju_with_fallback(other, released, command, args, include_e=True):
181
 
    """Fallback to released juju when 1.18 fails.
182
 
 
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
185
 
    format).
186
 
    """
187
 
    def call_juju(client):
188
 
        client.juju(command, args, include_e=include_e)
189
 
    return callback_with_fallback(other, released, call_juju)
190
 
 
191
 
 
192
 
def callback_with_fallback(other, released, callback):
193
 
    for client in [other, released]:
194
 
        try:
195
 
            callback(client)
196
 
        except CalledProcessError:
197
 
            if not client.version.startswith('1.18.'):
198
 
                raise
199
 
        else:
200
 
            break
201
 
 
202
 
 
203
 
def nice_tear_down(client):
204
 
    if client.is_jes_enabled():
205
 
        client.kill_controller()
206
 
    else:
207
 
        if client.destroy_environment(force=False) != 0:
208
 
            raise CalledProcessError(1, 'juju destroy-environment')
209
 
 
210
 
 
211
 
def has_agent(client, agent_id):
212
 
    return bool(agent_id in dict(client.get_status().agent_items()))
213
 
 
214
 
 
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):
219
 
            return
220
 
    else:
221
 
        raise AssertionError('Machine not destroyed: {}.'.format(agent_id))
222
 
 
223
 
 
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()
230
 
    if series:
231
 
        expected_codename = series
232
 
    else:
233
 
        expected_codename = client.env.config['default-series']
234
 
    if codename != expected_codename:
235
 
        raise AssertionError(
236
 
            'Series is {}, not {}'.format(codename, expected_codename))
237
 
 
238
 
 
239
 
def parse_args(argv=None):
240
 
    parser = ArgumentParser(description=dedent("""\
241
 
        Determine whether one juju version can control an environment created
242
 
        by another version.
243
 
    """))
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.')
249
 
    parser.add_argument(
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)
261
 
 
262
 
 
263
 
def main():
264
 
    args = parse_args()
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)
270
 
 
271
 
 
272
 
if __name__ == '__main__':
273
 
    main()