~andrewjbeach/juju-ci-tools/make-local-patcher

161 by Curtis Hovey
Windows and py3 compatability.
1
from __future__ import print_function
2
2 by Aaron Bentley
Added initial deploy_stack.
3
__metaclass__ = type
4
5
import yaml
6
7
from collections import defaultdict
8
from cStringIO import StringIO
9
from datetime import datetime, timedelta
15 by Aaron Bentley
Handle HTTPException.
10
import httplib
161 by Curtis Hovey
Windows and py3 compatability.
11
import os
16 by Aaron Bentley
Handle socket.error
12
import socket
2 by Aaron Bentley
Added initial deploy_stack.
13
import subprocess
14
import sys
139.1.2 by Aaron Bentley
Write stderr to a temp file.
15
import tempfile
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
16
from time import sleep
7 by Aaron Bentley
Validate wordpress page contents.
17
import urllib2
2 by Aaron Bentley
Added initial deploy_stack.
18
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
19
from jujuconfig import get_selected_environment
20
2 by Aaron Bentley
Added initial deploy_stack.
21
163 by Curtis Hovey
Extracted the windows command incase it needs to be reused.
22
WIN_JUJU_CMD = os.path.join('\\', 'Progra~2', 'Juju', 'juju.exe')
23
24
2 by Aaron Bentley
Added initial deploy_stack.
25
class ErroredUnit(Exception):
26
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
27
    def __init__(self, unit_name, state):
28
        msg = '%s is in state %s' % (unit_name, state)
19.1.14 by Aaron Bentley
More error handling fixes.
29
        Exception.__init__(self, msg)
2 by Aaron Bentley
Added initial deploy_stack.
30
31
19.1.32 by Aaron Bentley
Test until_timeout, making it a class to enable patching.
32
class until_timeout:
33
372.1.3 by Aaron Bentley
until_timeout yields remaining seconds
34
    """Yields remaining number of seconds.  Stops when timeout is reached.
12 by Aaron Bentley
Clean up.
35
19.1.32 by Aaron Bentley
Test until_timeout, making it a class to enable patching.
36
    :ivar timeout: Number of seconds to wait.
12 by Aaron Bentley
Clean up.
37
    """
19.1.32 by Aaron Bentley
Test until_timeout, making it a class to enable patching.
38
    def __init__(self, timeout):
39
        self.timeout = timeout
40
        self.start = self.now()
41
42
    def __iter__(self):
43
        return self
44
45
    @staticmethod
46
    def now():
47
        return datetime.now()
48
49
    def next(self):
372.1.3 by Aaron Bentley
until_timeout yields remaining seconds
50
        elapsed = self.now() - self.start
51
        remaining = self.timeout - elapsed.total_seconds()
52
        if remaining <= 0:
19.1.32 by Aaron Bentley
Test until_timeout, making it a class to enable patching.
53
            raise StopIteration
372.1.3 by Aaron Bentley
until_timeout yields remaining seconds
54
        return remaining
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
55
56
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
57
def yaml_loads(yaml_str):
58
    return yaml.safe_load(StringIO(yaml_str))
59
60
185.1.1 by Aaron Bentley
Fix cloud test handling of 'Unable to connect to environment.'
61
class CannotConnectEnv(subprocess.CalledProcessError):
62
63
    def __init__(self, e):
64
        super(CannotConnectEnv, self).__init__(e.returncode, e.cmd, e.output)
65
66
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
67
class JujuClientDevel:
68
    # This client is meant to work with the latest version of juju.
69
    # Subclasses will retain support for older versions of juju, so that the
70
    # latest version is easy to read, and older versions can be trivially
71
    # deleted.
72
107.1.2 by Aaron Bentley
Use full path when running under sudo.
73
    def __init__(self, version, full_path):
84.1.5 by Aaron Bentley
JujuClient stores the detected revision.
74
        self.version = version
107.1.2 by Aaron Bentley
Use full path when running under sudo.
75
        self.full_path = full_path
84.1.5 by Aaron Bentley
JujuClient stores the detected revision.
76
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
77
    @classmethod
78
    def get_version(cls):
107.1.2 by Aaron Bentley
Use full path when running under sudo.
79
        return subprocess.check_output(('juju', '--version')).strip()
80
81
    @classmethod
82
    def get_full_path(cls):
161 by Curtis Hovey
Windows and py3 compatability.
83
        if sys.platform == 'win32':
163 by Curtis Hovey
Extracted the windows command incase it needs to be reused.
84
            return WIN_JUJU_CMD
107.1.2 by Aaron Bentley
Use full path when running under sudo.
85
        return subprocess.check_output(('which', 'juju')).rstrip('\n')
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
86
87
    @classmethod
88
    def by_version(cls):
89
        version = cls.get_version()
107.1.2 by Aaron Bentley
Use full path when running under sudo.
90
        full_path = cls.get_full_path()
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
91
        if version.startswith('1.16'):
372.1.2 by Aaron Bentley
Support supplying timeout to calls.
92
            raise Exception('Unsupported juju: %s' % version)
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
93
        else:
107.1.2 by Aaron Bentley
Use full path when running under sudo.
94
            return JujuClientDevel(version, full_path)
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
95
372.1.2 by Aaron Bentley
Support supplying timeout to calls.
96
    def _full_args(self, environment, command, sudo, args, timeout=None):
196 by Curtis Hovey
Reverted most of the hacks for the juju 1.17.1 tests. Kept some test fixes.
97
        # sudo is not needed for devel releases.
19.1.33 by Aaron Bentley
Add JujuClient tests.
98
        e_arg = () if environment is None else ('-e', environment.environment)
372.1.2 by Aaron Bentley
Support supplying timeout to calls.
99
        if timeout is None:
100
            prefix = ()
101
        else:
102
            prefix = ('timeout', '%.2fs' % timeout)
103
        return prefix + ('juju', '--show-log', command,) + e_arg + args
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
104
107.1.2 by Aaron Bentley
Use full path when running under sudo.
105
    def bootstrap(self, environment):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
106
        """Bootstrap, using sudo if necessary."""
139 by Curtis Hovey
Added hpcloud attribute based on hp auth-url. hpcloud uses 4G to ensure mysql starts.
107
        if environment.hpcloud:
108
            constraints = 'mem=4G'
109
        else:
110
            constraints = 'mem=2G'
111
        self.juju(environment, 'bootstrap', ('--constraints', constraints),
107.1.3 by Aaron Bentley
Remove unnecessary --upload-tools.
112
                  environment.needs_sudo())
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
113
107.1.2 by Aaron Bentley
Use full path when running under sudo.
114
    def destroy_environment(self, environment):
115
        self.juju(
197 by Curtis Hovey
Alwayd destroy-environment with --force.
116
            None, 'destroy-environment',
117
            (environment.environment, '--force', '-y'),
19.1.22 by Aaron Bentley
Fix up destroy-environment.
118
            environment.needs_sudo(), check=False)
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
119
372.1.2 by Aaron Bentley
Support supplying timeout to calls.
120
    def get_juju_output(self, environment, command, *args, **kwargs):
121
        args = self._full_args(environment, command, False, args,
122
                               timeout=kwargs.get('timeout'))
139.1.2 by Aaron Bentley
Write stderr to a temp file.
123
        with tempfile.TemporaryFile() as stderr:
124
            try:
125
                return subprocess.check_output(args, stderr=stderr)
126
            except subprocess.CalledProcessError as e:
127
                stderr.seek(0)
128
                e.stderr = stderr.read()
218 by Curtis Hovey
Convert MissingOrIncorrectVersionHeader to CannotConnectEnv.
129
                if ('Unable to connect to environment' in e.stderr
219 by Curtis Hovey
Convert 307 status to CannotConnectEnv.
130
                        or 'MissingOrIncorrectVersionHeader' in e.stderr
131
                        or '307: Temporary Redirect' in e.stderr):
218 by Curtis Hovey
Convert MissingOrIncorrectVersionHeader to CannotConnectEnv.
132
                    raise CannotConnectEnv(e)
133
                print('!!! ' + e.stderr)
134
                raise
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
135
107.1.2 by Aaron Bentley
Use full path when running under sudo.
136
    def get_status(self, environment):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
137
        """Get the current status as a dict."""
365.1.1 by Aaron Bentley
Restore timeouts to previous values.
138
        for ignored in until_timeout(60):
331.1.1 by Aaron Bentley
juju status retries on error for 30 seconds.
139
            try:
140
                return Status(yaml_loads(
141
                    self.get_juju_output(environment, 'status')))
142
            except subprocess.CalledProcessError as e:
143
                pass
144
        raise Exception(
145
            'Timed out waiting for juju status to succeed: %s' % e)
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
146
353 by Curtis Hovey
Added get_env_option().
147
    def get_env_option(self, environment, option):
354 by Curtis Hovey
Added set_env_option().
148
        """Return the value of the environment's configured option."""
361 by Curtis Hovey
Use get_juju_output() to get the env option.
149
        return self.get_juju_output(environment, 'get-env', option)
353 by Curtis Hovey
Added get_env_option().
150
357 by Curtis Hovey
Separate option from values. The callee doesn't need to
151
    def set_env_option(self, environment, option, value):
152
        """Set the value of the option in the environment."""
153
        option_value = "%s=%s" % (option, value)
354 by Curtis Hovey
Added set_env_option().
154
        return self.juju(environment, 'set-env', (option_value,))
353 by Curtis Hovey
Added get_env_option().
155
107.1.2 by Aaron Bentley
Use full path when running under sudo.
156
    def juju(self, environment, command, args, sudo=False, check=True):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
157
        """Run a command under juju for the current environment."""
107.1.2 by Aaron Bentley
Use full path when running under sudo.
158
        args = self._full_args(environment, command, sudo, args)
161 by Curtis Hovey
Windows and py3 compatability.
159
        print(' '.join(args))
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
160
        sys.stdout.flush()
19.1.19 by Aaron Bentley
Ignore status code for destroy_environment.
161
        if check:
19.1.23 by Aaron Bentley
Return when running check_call.
162
            return subprocess.check_call(args)
19.1.19 by Aaron Bentley
Ignore status code for destroy_environment.
163
        return subprocess.call(args)
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
164
165
34.1.3 by Aaron Bentley
Add status and environment tests.
166
class Status:
167
168
    def __init__(self, status):
169
        self.status = status
170
171
    def agent_items(self):
172
        for machine_name, machine in sorted(self.status['machines'].items()):
173
            yield machine_name, machine
174
        for service in sorted(self.status['services'].values()):
175
            for unit_name, unit in service.get('units', {}).items():
176
                yield unit_name, unit
177
178
    def agent_states(self):
179
        """Map agent states to the units and machines in those states."""
180
        states = defaultdict(list)
181
        for item_name, item in self.agent_items():
182
            states[item.get('agent-state', 'no-agent')].append(item_name)
183
        return states
184
185
    def check_agents_started(self, environment_name):
186
        """Check whether all agents are in the 'started' state.
187
188
        If not, return agent_states output.  If so, return None.
189
        If an error is encountered for an agent, raise ErroredUnit
190
        """
191
        # Look for errors preventing an agent from being installed
192
        for item_name, item in self.agent_items():
193
            state_info = item.get('agent-state-info', '')
194
            if 'error' in state_info:
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
195
                raise ErroredUnit(item_name, state_info)
34.1.3 by Aaron Bentley
Add status and environment tests.
196
        states = self.agent_states()
197
        if states.keys() == ['started']:
198
            return None
199
        for state, entries in states.items():
200
            if 'error' in state:
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
201
                raise ErroredUnit(entries[0],  state)
34.1.3 by Aaron Bentley
Add status and environment tests.
202
        return states
203
84.1.4 by Aaron Bentley
Move version-dicting to jujupy.
204
    def get_agent_versions(self):
205
        versions = defaultdict(set)
206
        for item_name, item in self.agent_items():
207
            versions[item.get('agent-version', 'unknown')].add(item_name)
208
        return versions
209
34.1.3 by Aaron Bentley
Add status and environment tests.
210
2 by Aaron Bentley
Added initial deploy_stack.
211
class Environment:
212
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
213
    def __init__(self, environment, client=None, config=None):
2 by Aaron Bentley
Added initial deploy_stack.
214
        self.environment = environment
34.1.3 by Aaron Bentley
Add status and environment tests.
215
        self.client = client
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
216
        self.config = config
217
        if self.config is not None:
218
            self.local = bool(self.config.get('type') == 'local')
139 by Curtis Hovey
Added hpcloud attribute based on hp auth-url. hpcloud uses 4G to ensure mysql starts.
219
            self.hpcloud = bool(
220
                'hpcloudsvc' in self.config.get('auth-url', ''))
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
221
        else:
222
            self.local = False
139 by Curtis Hovey
Added hpcloud attribute based on hp auth-url. hpcloud uses 4G to ensure mysql starts.
223
            self.hpcloud = False
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
224
225
    @classmethod
226
    def from_config(cls, name):
227
        client = JujuClientDevel.by_version()
228
        return cls(name, client, get_selected_environment(name)[0])
2 by Aaron Bentley
Added initial deploy_stack.
229
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
230
    def needs_sudo(self):
84.1.7 by Aaron Bentley
Append .1 to local-provider version numbers.
231
        return self.local
2 by Aaron Bentley
Added initial deploy_stack.
232
19.1.11 by Aaron Bentley
Update bootstrap to sudo for local provider.
233
    def bootstrap(self):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
234
        return self.client.bootstrap(self)
19.1.11 by Aaron Bentley
Update bootstrap to sudo for local provider.
235
112.1.5 by Aaron Bentley
Use config to determine whether upgrades are local.
236
    def upgrade_juju(self):
112.1.7 by Aaron Bentley
Fix upgrade command.
237
        args = ('--version', self.get_matching_agent_version(no_build=True))
112.1.5 by Aaron Bentley
Use config to determine whether upgrades are local.
238
        if self.local:
239
            args += ('--upload-tools',)
240
        self.client.juju(self, 'upgrade-juju', args)
241
19.1.21 by Aaron Bentley
Provide destroy_environment on Environment itself.
242
    def destroy_environment(self):
243
        return self.client.destroy_environment(self)
244
339.1.1 by Aaron Bentley
Use deploy method, tweak exception reporting.
245
    def deploy(self, charm):
246
        args = (charm,)
247
        return self.juju('deploy', *args)
248
2 by Aaron Bentley
Added initial deploy_stack.
249
    def juju(self, command, *args):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
250
        return self.client.juju(self, command, args)
251
252
    def get_status(self):
253
        return self.client.get_status(self)
2 by Aaron Bentley
Added initial deploy_stack.
254
349 by Curtis Hovey
Allow the caller to specify the timeout.
255
    def wait_for_started(self, timeout=1200):
12 by Aaron Bentley
Clean up.
256
        """Wait until all unit/machine agents are 'started'."""
349 by Curtis Hovey
Allow the caller to specify the timeout.
257
        for ignored in until_timeout(timeout):
217 by Curtis Hovey
Continue when a CannotConnectEnv error is reaised from get_status.
258
            try:
259
                status = self.get_status()
260
            except CannotConnectEnv:
261
                print('Supressing "Unable to connect to environment"')
262
                continue
34.1.3 by Aaron Bentley
Add status and environment tests.
263
            states = status.check_agents_started(self.environment)
264
            if states is None:
19.1.1 by Aaron Bentley
Add wait_for_agent_update.py
265
                break
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
266
            print(format_listing(states, 'started'))
4 by Aaron Bentley
Tweak output.
267
            sys.stdout.flush()
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
268
        else:
73.1.1 by Aaron Bentley
Make timeout exception more specific.
269
            raise Exception('Timed out waiting for agents to start in %s.' %
270
                            self.environment)
19.1.3 by Aaron Bentley
Fix wait_for_started logic.
271
        return status
2 by Aaron Bentley
Added initial deploy_stack.
272
84.1.6 by Aaron Bentley
Ensure agent-version match.
273
    def wait_for_version(self, version):
365.1.1 by Aaron Bentley
Restore timeouts to previous values.
274
        for ignored in until_timeout(300):
139.1.1 by Aaron Bentley
Try handling Unable to connect to environment gracefully.
275
            try:
352 by Curtis Hovey
Fix test by not passing unknown values to get_status().
276
                versions = self.get_status().get_agent_versions()
196 by Curtis Hovey
Reverted most of the hacks for the juju 1.17.1 tests. Kept some test fixes.
277
            except CannotConnectEnv:
185.1.1 by Aaron Bentley
Fix cloud test handling of 'Unable to connect to environment.'
278
                print('Supressing "Unable to connect to environment"')
279
                continue
84.1.6 by Aaron Bentley
Ensure agent-version match.
280
            if versions.keys() == [version]:
281
                break
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
282
            print(format_listing(versions, version))
84.1.6 by Aaron Bentley
Ensure agent-version match.
283
            sys.stdout.flush()
284
        else:
285
            raise Exception('Some versions did not update.')
286
112.1.7 by Aaron Bentley
Fix upgrade command.
287
    def get_matching_agent_version(self, no_build=False):
84.1.7 by Aaron Bentley
Append .1 to local-provider version numbers.
288
        version_number = self.client.version.split('-')[0]
233.1.18 by Aaron Bentley
Stop using --upload-tools for manual provider.
289
        if not no_build and self.local:
84.1.7 by Aaron Bentley
Append .1 to local-provider version numbers.
290
            version_number += '.1'
291
        return version_number
84.1.6 by Aaron Bentley
Ensure agent-version match.
292
353 by Curtis Hovey
Added get_env_option().
293
    def set_testing_tools_metadata_url(self):
355 by Curtis Hovey
Added set_testing_tools_metadata_url().
294
        url = self.client.get_env_option(self, 'tools-metadata-url')
358 by Curtis Hovey
Do not change the tools-metadata-url if it already has 'testing'
295
        if 'testing' not in url:
296
            testing_url = url.replace('/tools', '/testing/tools')
297
            self.client.set_env_option(self, 'tools-metadata-url',  testing_url)
353 by Curtis Hovey
Added get_env_option().
298
2 by Aaron Bentley
Added initial deploy_stack.
299
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
300
def format_listing(listing, expected):
19.1.1 by Aaron Bentley
Add wait_for_agent_update.py
301
    value_listing = []
302
    for value, entries in listing.items():
303
        if value == expected:
304
            continue
305
        value_listing.append('%s: %s' % (value, ', '.join(entries)))
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
306
    return ' | '.join(value_listing)
307
308
309
def check_wordpress(host):
12 by Aaron Bentley
Clean up.
310
    """"Check whether Wordpress has come up successfully.
311
312
    Times out after 30 seconds.
313
    """
7 by Aaron Bentley
Validate wordpress page contents.
314
    welcome_text = ('Welcome to the famous five minute WordPress'
315
                    ' installation process!')
11 by Aaron Bentley
Add url to error.
316
    url = 'http://%s/wp-admin/install.php' % host
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
317
    for ignored in until_timeout(30):
318
        try:
11 by Aaron Bentley
Add url to error.
319
            page = urllib2.urlopen(url)
16 by Aaron Bentley
Handle socket.error
320
        except (urllib2.URLError, httplib.HTTPException, socket.error):
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
321
            pass
322
        else:
323
            if welcome_text in page.read():
324
                break
12 by Aaron Bentley
Clean up.
325
        # Let's not DOS wordpress
10 by Aaron Bentley
Give wordpress some time to get the welcome page together.
326
        sleep(1)
327
    else:
19.1.24 by Aaron Bentley
Mention environment in more places.
328
        raise Exception(
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
329
            'Cannot get welcome screen at %s' % (url))