Merge lp:~natefinch/juju-ci-tools/logrot into lp:juju-ci-tools

Proposed by Nate Finch
Status: Merged
Merged at revision: 976
Proposed branch: lp:~natefinch/juju-ci-tools/logrot
Merge into: lp:juju-ci-tools
Diff against target: 1215 lines (+661/-135)
8 files modified
assess_log_rotation.py (+228/-0)
assess_recovery.py (+4/-21)
check_blockers.py (+4/-6)
jujupy.py (+98/-5)
test_assess_log_rotation.py (+154/-0)
test_assess_recovery.py (+0/-60)
test_deploy_stack.py (+17/-17)
test_jujupy.py (+156/-26)
To merge this branch: bzr merge lp:~natefinch/juju-ci-tools/logrot
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+259750@code.launchpad.net

Description of the change

Implements action do and fetch in jujupy, and uses them in tests for log rotation. Requires new charm in the repository, which will be proposed shortly.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Environment is deprecated. It should not be used for new scripts, and it should not be extended.

If you're going to remove '-e', you need to specify the environment somehow. Since "action fetch" and "action do" both support -e, I don't think you actually want to remove it. You just want to make the command handling smarter, so that it puts the -e after "do" or "fetch".

Please allow the juju under test to be specified at the commandline.

Please use temp_bootstrap_env(). At minimum, that will ensure that 'test-mode' is always True, but it will also allow us to support configuration variations such as changing the tools-url.

It would be nice if the test was self-contained. It is important that unit-fill-logs-0 and fill-machine be actions, or could they be run via "juju run" or "juju ssh"? That would allow the test script to use a generic charm instead.

Please add unit tests for log_rotation.py.

Please rename it to something that indicates it is a test. I suggest "assess_log_rotation". ("test" itself indicates that it is a unit test).

Juju 1.22 is still under development. Please ensure that these actions work with 1.22 by specifying the appropriate feature flag when the version is 1.22. See EnvJujuClient24 for an example.

review: Needs Fixing
Revision history for this message
Nate Finch (natefinch) wrote :

The test intentionally uses a charm, because it represents a real world situation where the charm is the thing outputting a lot of logging. I don't know that I could reasonably create a juju run command that would do the same thing. This seemed like the most easy to understand way to get the job done. I could use juju shh to get the size of the logs, but since I already had the charm and actions, it seemed easy enough to just make that another action.

Revision history for this message
Aaron Bentley (abentley) wrote :

> The test intentionally uses a charm, because it represents a real world
> situation where the charm is the thing outputting a lot of logging. I don't
> know that I could reasonably create a juju run command that would do the same
> thing.

You can log in a hook context and juju run has a hook context, though, right?

I'll accept a version that is not self-contained, I just wondered whether it was possible.

Revision history for this message
Nate Finch (natefinch) wrote :

I think I addressed all your concerns. I ended up refactoring some of the helper functions in assess_recovery, since they were needed for my test as well.

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks.

Please don't add things to deploy_stack unless they are used by deploy_stack (i.e. deploy_job) itself. (The history is that deploy_stack was the first script, so some of its functionality has been reused in other scripts.)

Please don't add anything at all to jujuconfig. That is essentially an import from https://launchpad.net/juju-tools, and it would be best if they don't diverge.

I see that you have not addressed the inline comments about KeyError and tuple formattting. Please go back and make sure you've addressed all the inline comments.

Revision history for this message
Nate Finch (natefinch) wrote :

Sorry about missing your inline comments... I haven't done a review on launchpad in about a year, so forgot that I had to scroll down to look for more comments.

I have a patch coming that will address all these issues.

lp:~natefinch/juju-ci-tools/logrot updated
961. By Nate Finch

more review changes

Revision history for this message
Aaron Bentley (abentley) wrote :

I think that you haven't run "make lint", because I see some lines longer than 79 characters. Please run "make lint" and fix any issues you find.

Please document all functions in assess_log_rotation.py

Please test parse_args.

See also inline comments.

review: Needs Fixing
Revision history for this message
Aaron Bentley (abentley) wrote :

Also, I think you missed this:
Juju 1.22 is still under development. Please ensure that these actions work with 1.22 by specifying the appropriate feature flag when the version is 1.22. See EnvJujuClient24 for an example.

lp:~natefinch/juju-ci-tools/logrot updated
962. By Nate Finch

more code review changes

963. By Nate Finch

merge

964. By Nate Finch

set longer timeout for fill log actions, and fix -e testing problem

Revision history for this message
Nate Finch (natefinch) wrote :

Ok, for real now, I think I addressed everything.

lp:~natefinch/juju-ci-tools/logrot updated
965. By Nate Finch

remove unneeded wait

Revision history for this message
Aaron Bentley (abentley) wrote :

This is almost landable, but there are still a few issues.

review: Needs Fixing
Revision history for this message
Aaron Bentley (abentley) wrote :

Also, TestEnvJujuClient22 needs client_class set to EnvJujuClient22.

lp:~natefinch/juju-ci-tools/logrot updated
966. By Nate Finch

more code review changes

Revision history for this message
Nate Finch (natefinch) wrote :

Ok, updated once again. Addressed your concerns.

lp:~natefinch/juju-ci-tools/logrot updated
967. By Nate Finch

one more fix to TestEnvJujuClient22

Revision history for this message
Aaron Bentley (abentley) wrote :

Thank you for you changes. This looks landable.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'assess_log_rotation.py'
2--- assess_log_rotation.py 1970-01-01 00:00:00 +0000
3+++ assess_log_rotation.py 2015-06-03 15:41:41 +0000
4@@ -0,0 +1,228 @@
5+#!/usr/bin/env python
6+from __future__ import print_function
7+
8+__metaclass__ = type
9+
10+from argparse import ArgumentParser
11+from datetime import datetime
12+import logging
13+import re
14+
15+from deploy_stack import (
16+ dump_env_logs,
17+ get_machine_dns_name,
18+)
19+from jujuconfig import (
20+ get_juju_home,
21+)
22+from jujupy import (
23+ make_client,
24+ parse_new_state_server_from_error,
25+ temp_bootstrap_env,
26+ yaml_loads,
27+)
28+from utility import (
29+ print_now,
30+)
31+
32+
33+class LogRotateError(Exception):
34+
35+ ''' LogRotate test Exception base class. '''
36+
37+ def __init__(self, message):
38+ super(LogRotateError, self).__init__(message)
39+
40+
41+def test_unit_rotation(client):
42+ """Tests unit log rotation."""
43+ test_rotation(client,
44+ "/var/log/juju/unit-fill-logs-0.log",
45+ "unit-fill-logs-0",
46+ "fill-unit",
47+ "unit-size",
48+ "megs=300")
49+
50+
51+def test_machine_rotation(client):
52+ """Tests machine log rotation."""
53+ test_rotation(client,
54+ "/var/log/juju/machine-1.log",
55+ "machine-1",
56+ "fill-machine",
57+ "machine-size", "megs=300", "machine=1")
58+
59+
60+def test_rotation(client, logfile, prefix, fill_action, size_action, *args):
61+ """A reusable help for testing log rotation.log
62+
63+ Deploys the fill-logs charm and uses it to fill the machine or unit log and
64+ test that the logs roll over correctly.
65+ """
66+
67+ # the rotation point should be 300 megs, so let's make sure we hit that.hit
68+ # we'll obviously already have some data in the logs, so adding exactly
69+ # 300megs should do the trick.
70+
71+ # we run do_fetch here so that we wait for fill-logs to finish.
72+ client.action_do_fetch("fill-logs/0", fill_action, "3m", *args)
73+ out = client.action_do_fetch("fill-logs/0", size_action)
74+ action_output = yaml_loads(out)
75+
76+ # Now we should have one primary log file, and one backup log file.
77+ # The backup should be approximately 300 megs.
78+ # The primary should be below 300.
79+
80+ check_log0(logfile, action_output)
81+ check_expected_backup("log1", prefix, action_output)
82+
83+ # we should only have one backup, not two.
84+ check_for_extra_backup("log2", action_output)
85+
86+ # do it all again, this should generate a second backup.
87+
88+ client.action_do_fetch("fill-logs/0", fill_action, "3m", *args)
89+ out = client.action_do_fetch("fill-logs/0", size_action)
90+ action_output = yaml_loads(out)
91+
92+ # we should have two backups.
93+ check_log0(logfile, action_output)
94+ check_expected_backup("log1", prefix, action_output)
95+ check_expected_backup("log2", prefix, action_output)
96+
97+ check_for_extra_backup("log3", action_output)
98+
99+ # one more time... we should still only have 2 backups and primary
100+
101+ client.action_do_fetch("fill-logs/0", fill_action, "3m", *args)
102+ out = client.action_do_fetch("fill-logs/0", size_action)
103+ action_output = yaml_loads(out)
104+
105+ check_log0(logfile, action_output)
106+ check_expected_backup("log1", prefix, action_output)
107+ check_expected_backup("log2", prefix, action_output)
108+
109+ # we should have two backups.
110+ check_for_extra_backup("log3", action_output)
111+
112+
113+def check_for_extra_backup(logname, action_output):
114+ """Check that there are no extra backup files left behind."""
115+ log = action_output["results"]["result-map"].get(logname)
116+ if log is None:
117+ # this is correct
118+ return
119+ # log exists.
120+ name = log.get("name")
121+ if name is None:
122+ name = "(no name)"
123+ raise LogRotateError("Extra backup log after rotation: " + name)
124+
125+
126+def check_expected_backup(key, logprefix, action_output):
127+ """Check that there the expected backup files exists and is close to 300MB.
128+ """
129+ log = action_output["results"]["result-map"].get(key)
130+ if log is None:
131+ raise LogRotateError(
132+ "Missing backup log '{}' after rotation.".format(key))
133+
134+ backup_pattern = "/var/log/juju/%s-(.+?)\.log" % logprefix
135+
136+ log_name = log["name"]
137+ matches = re.match(backup_pattern, log_name)
138+ if matches is None:
139+ raise LogRotateError(
140+ "Rotated log '%s' does not match pattern '%s'." %
141+ (log_name, backup_pattern))
142+
143+ size = int(log["size"])
144+ if size < 299 or size > 301:
145+ raise LogRotateError(
146+ "Backup log '%s' should be ~300MB, but is %sMB." %
147+ (log_name, size))
148+
149+ dt = matches.groups()[0]
150+ dt_pattern = "%Y-%m-%dT%H-%M-%S.%f"
151+
152+ try:
153+ # note - we have to use datetime's strptime because time's doesn't
154+ # support partial seconds.
155+ dt = datetime.strptime(dt, dt_pattern)
156+ except Exception:
157+ raise LogRotateError(
158+ "Log for %s has invalid datetime appended: %s" % (log_name, dt))
159+
160+
161+def check_log0(expected, action_output):
162+ """Check that log0 exists and is not over 299MB"""
163+ log = action_output["results"]["result-map"].get("log0")
164+ if log is None:
165+ raise LogRotateError("No log returned from size action.")
166+
167+ name = log["name"]
168+ if name != expected:
169+ raise LogRotateError(
170+ "Wrong unit name: Expected: %s, actual: %s" % (expected, name))
171+
172+ size = int(log["size"])
173+ if size > 299:
174+ raise LogRotateError(
175+ "Log0 too big. Expected < 300MB, got: %sMB" % size)
176+
177+
178+def parse_args(argv=None):
179+ """Parse all arguments."""
180+ parser = ArgumentParser('Test log rotation.')
181+ parser.add_argument(
182+ '--debug', action='store_true', default=False,
183+ help='Use --debug juju logging.')
184+ parser.add_argument(
185+ 'agent',
186+ help='Which agent log rotation to test.',
187+ choices=['machine', 'unit'])
188+ parser.add_argument(
189+ 'juju_path', help='Directory your juju binary lives in.')
190+ parser.add_argument(
191+ 'env_name', help='Juju environment name to run tests in.')
192+ parser.add_argument('logs', help='Directory to store logs in.')
193+ parser.add_argument(
194+ 'temp_env_name', nargs='?',
195+ help='Temporary environment name to use for this test.')
196+ return parser.parse_args(argv)
197+
198+
199+def main():
200+ args = parse_args()
201+ log_dir = args.logs
202+
203+ client = make_client(
204+ args.juju_path, args.debug, args.env_name, args.temp_env_name)
205+ client.destroy_environment()
206+ juju_home = get_juju_home()
207+ try:
208+ with temp_bootstrap_env(juju_home, client):
209+ client.bootstrap()
210+ bootstrap_host = get_machine_dns_name(client, 0)
211+ client.get_status(60)
212+ client.juju("deploy", ('local:trusty/fill-logs',))
213+
214+ if args.agent == "unit":
215+ test_unit_rotation(client)
216+ if args.agent == "machine":
217+ test_machine_rotation(client)
218+ except Exception as e:
219+ logging.exception(e)
220+ try:
221+ if bootstrap_host is None:
222+ bootstrap_host = parse_new_state_server_from_error(e)
223+ dump_env_logs(client, bootstrap_host, log_dir)
224+ except Exception as e:
225+ print_now("exception while dumping logs:\n")
226+ logging.exception(e)
227+ finally:
228+ client.destroy_environment()
229+
230+
231+if __name__ == '__main__':
232+ main()
233
234=== modified file 'assess_recovery.py'
235--- assess_recovery.py 2015-02-11 17:42:41 +0000
236+++ assess_recovery.py 2015-06-03 15:41:41 +0000
237@@ -20,16 +20,16 @@
238 from jujuconfig import (
239 get_jenv_path,
240 get_juju_home,
241- )
242+)
243 from jujupy import (
244- EnvJujuClient,
245- SimpleEnvironment,
246 temp_bootstrap_env,
247 until_timeout,
248+ make_client,
249+ parse_new_state_server_from_error,
250 )
251 from substrate import (
252 terminate_instances,
253- )
254+)
255 from utility import (
256 ensure_deleted,
257 print_now,
258@@ -133,14 +133,6 @@
259 print_now("PASS")
260
261
262-def parse_new_state_server_from_error(error):
263- output = str(error) + getattr(error, 'output', '')
264- matches = re.findall(r'Attempting to connect to (.*):22', output)
265- if matches:
266- return matches[-1]
267- return None
268-
269-
270 def parse_args(argv=None):
271 parser = ArgumentParser('Test recovery strategies.')
272 parser.add_argument(
273@@ -167,15 +159,6 @@
274 return parser.parse_args(argv)
275
276
277-def make_client(juju_path, debug, env_name, temp_env_name):
278- env = SimpleEnvironment.from_config(env_name)
279- if temp_env_name is not None:
280- env.environment = temp_env_name
281- env.config['name'] = temp_env_name
282- full_path = os.path.join(juju_path, 'juju')
283- return EnvJujuClient.by_version(env, full_path, debug)
284-
285-
286 def main():
287 args = parse_args()
288 log_dir = args.logs
289
290=== modified file 'check_blockers.py'
291--- check_blockers.py 2015-05-15 19:45:42 +0000
292+++ check_blockers.py 2015-06-03 15:41:41 +0000
293@@ -51,12 +51,10 @@
294 subparsers = parser.add_subparsers(help='sub-command help', dest="command")
295 check_parser = subparsers.add_parser(
296 'check', help='Check if merges are blocked for a branch.')
297- check_parser.add_argument(
298- 'branch', default='master', nargs='?',
299- help='The branch to merge into.')
300- check_parser.add_argument(
301- 'pull_request', default=None, nargs='?',
302- help='The pull request to be merged')
303+ check_parser.add_argument('branch', default='master', nargs='?',
304+ help='The branch to merge into.')
305+ check_parser.add_argument('pull_request', default=None, nargs='?',
306+ help='The pull request to be merged')
307 update_parser = subparsers.add_parser(
308 'update', help='Update blocking for a branch that passed CI.')
309 update_parser.add_argument(
310
311=== modified file 'jujupy.py'
312--- jujupy.py 2015-06-01 21:06:33 +0000
313+++ jujupy.py 2015-06-03 15:41:41 +0000
314@@ -6,7 +6,7 @@
315 from contextlib import (
316 contextmanager,
317 nested,
318- )
319+)
320 from cStringIO import StringIO
321 from datetime import timedelta
322 import errno
323@@ -25,7 +25,7 @@
324 get_jenv_path,
325 get_juju_home,
326 get_selected_environment,
327- )
328+)
329 from utility import (
330 check_free_disk_space,
331 ensure_deleted,
332@@ -34,7 +34,7 @@
333 scoped_environ,
334 temp_dir,
335 until_timeout,
336- )
337+)
338
339
340 WIN_JUJU_CMD = os.path.join('\\', 'Progra~2', 'Juju', 'juju.exe')
341@@ -42,6 +42,14 @@
342 JUJU_DEV_FEATURE_FLAGS = 'JUJU_DEV_FEATURE_FLAGS'
343
344
345+def parse_new_state_server_from_error(error):
346+ output = str(error) + getattr(error, 'output', '')
347+ matches = re.findall(r'Attempting to connect to (.*):22', output)
348+ if matches:
349+ return matches[-1]
350+ return None
351+
352+
353 class ErroredUnit(Exception):
354
355 def __init__(self, unit_name, state):
356@@ -55,6 +63,15 @@
357 return yaml.safe_load(StringIO(yaml_str))
358
359
360+def make_client(juju_path, debug, env_name, temp_env_name):
361+ env = SimpleEnvironment.from_config(env_name)
362+ if temp_env_name is not None:
363+ env.environment = temp_env_name
364+ env.config['name'] = temp_env_name
365+ full_path = os.path.join(juju_path, 'juju')
366+ return EnvJujuClient.by_version(env, full_path, debug)
367+
368+
369 class CannotConnectEnv(subprocess.CalledProcessError):
370
371 def __init__(self, e):
372@@ -156,6 +173,8 @@
373 full_path = os.path.abspath(juju_path)
374 if version.startswith('1.16'):
375 raise Exception('Unsupported juju: %s' % version)
376+ elif re.match('^1\.22[.-]', version):
377+ return EnvJujuClient22(env, version, full_path, debug=debug)
378 elif re.match('^1\.24[.-]', version):
379 return EnvJujuClient24(env, version, full_path, debug=debug)
380 elif re.match('^1\.25[.-]', version):
381@@ -174,6 +193,10 @@
382 else:
383 prefix = ('timeout', '%.2fs' % timeout)
384 logging = '--debug' if self.debug else '--show-log'
385+
386+ # we split the command here so that the caller can control where the -e
387+ # <env> flag goes. Everything in the command string is put before the
388+ # -e flag.
389 command = command.split()
390 return prefix + ('juju', logging,) + tuple(command) + e_arg + args
391
392@@ -197,6 +220,7 @@
393 env['PATH'])
394 if juju_home is not None:
395 env['JUJU_HOME'] = juju_home
396+
397 return env
398
399 def get_bootstrap_args(self, upload_tools):
400@@ -243,8 +267,15 @@
401 ensure_deleted(jenv_path)
402
403 def get_juju_output(self, command, *args, **kwargs):
404+ """Call a juju command and return the output.
405+
406+ Sub process will be called as 'juju <command> <args> <kwargs>'. Note
407+ that <command> may be a space delimited list of arguments. The -e
408+ <environment> flag will be placed after <command> and before args.
409+ """
410 args = self._full_args(command, False, args,
411- timeout=kwargs.get('timeout'))
412+ timeout=kwargs.get('timeout'),
413+ include_e=kwargs.get('include_e', True))
414 env = self._shell_environ()
415 with tempfile.TemporaryFile() as stderr:
416 try:
417@@ -478,6 +509,67 @@
418 print_now("State-Server backup at %s" % backup_file_path)
419 return backup_file_path
420
421+ def action_fetch(self, id, action=None, timeout="1m"):
422+ """Fetches the results of the action with the given id.
423+
424+ Will wait for up to 1 minute for the action results.
425+ The action name here is just used for an more informational error in
426+ cases where it's available.
427+ Returns the yaml output of the fetched action.
428+ """
429+ # the command has to be "action fetch" so that the -e <env> args are
430+ # placed after "fetch", since that's where action requires them to be.
431+ out = self.get_juju_output("action fetch", id, "--wait", timeout)
432+ status = yaml_loads(out)["status"]
433+ if status != "completed":
434+ name = ""
435+ if action is not None:
436+ name = " " + action
437+ raise Exception(
438+ "timed out waiting for action%s to complete during fetch" %
439+ name)
440+ return out
441+
442+ def action_do(self, unit, action, *args):
443+ """Performs the given action on the given unit.
444+
445+ Action params should be given as args in the form foo=bar.
446+ Returns the id of the queued action.
447+ """
448+ args = (unit, action) + args
449+
450+ # the command has to be "action do" so that the -e <env> args are
451+ # placed after "do", since that's where action requires them to be.
452+ output = self.get_juju_output("action do", *args)
453+ action_id_pattern = re.compile(
454+ 'Action queued with id: ([a-f0-9\-]{36})')
455+ match = action_id_pattern.search(output)
456+ if match is None:
457+ raise Exception("Action id not found in output: %s" %
458+ output)
459+ return match.group(1)
460+
461+ def action_do_fetch(self, unit, action, timeout="1m", *args):
462+ """Performs the given action on the given unit and waits for the results.
463+
464+ Action params should be given as args in the form foo=bar.
465+ Returns the yaml output of the action.
466+ """
467+ id = self.action_do(unit, action, *args)
468+ return self.action_fetch(id, action, timeout)
469+
470+
471+class EnvJujuClient22(EnvJujuClient):
472+
473+ def _shell_environ(self, juju_home=None):
474+ """Generate a suitable shell environment.
475+
476+ Juju's directory must be in the PATH to support plugins.
477+ """
478+ env = super(EnvJujuClient22, self)._shell_environ(juju_home)
479+ env[JUJU_DEV_FEATURE_FLAGS] = 'actions'
480+ return env
481+
482
483 class EnvJujuClient25(EnvJujuClient):
484
485@@ -493,7 +585,8 @@
486
487
488 class EnvJujuClient24(EnvJujuClient25):
489- """Currently, same feature set as juju 2.5"""
490+
491+ """Currently, same feature set as juju 25"""
492
493
494 def get_local_root(juju_home, env):
495
496=== added file 'test_assess_log_rotation.py'
497--- test_assess_log_rotation.py 1970-01-01 00:00:00 +0000
498+++ test_assess_log_rotation.py 2015-06-03 15:41:41 +0000
499@@ -0,0 +1,154 @@
500+from argparse import Namespace
501+from unittest import TestCase
502+from assess_log_rotation import (
503+ check_for_extra_backup,
504+ check_expected_backup,
505+ check_log0,
506+ LogRotateError,
507+ parse_args,
508+)
509+from jujupy import yaml_loads
510+
511+good_yaml = \
512+ """
513+results:
514+ result-map:
515+ log0:
516+ name: /var/log/juju/unit-fill-logs-0.log
517+ size: "25"
518+ log1:
519+ name: /var/log/juju/unit-fill-logs-0-2015-05-21T09-57-03.123.log
520+ size: "299"
521+ log1:
522+ name: /var/log/juju/unit-fill-logs-0-2015-05-22T12-57-03.123.log
523+ size: "300"
524+status: completed
525+timing:
526+ completed: 2015-05-21 09:57:03 -0400 EDT
527+ enqueued: 2015-05-21 09:56:59 -0400 EDT
528+ started: 2015-05-21 09:57:02 -0400 EDT
529+"""
530+
531+good_obj = yaml_loads(good_yaml)
532+
533+big_yaml = \
534+ """
535+results:
536+ result-map:
537+ log0:
538+ name: /var/log/juju/unit-fill-logs-0.log
539+ size: "400"
540+ log1:
541+ name: /var/log/juju/unit-fill-logs-0-2015-05-21T09-57-03.123.log
542+ size: "400"
543+ log2:
544+ name: /var/log/juju/unit-fill-logs-0-not-a-valid-timestamp.log
545+ size: "299"
546+ log3:
547+ name: something-just-plain-bad.log
548+ size: "299"
549+status: completed
550+timing:
551+ completed: 2015-05-21 09:57:03 -0400 EDT
552+ enqueued: 2015-05-21 09:56:59 -0400 EDT
553+ started: 2015-05-21 09:57:02 -0400 EDT
554+"""
555+
556+big_obj = yaml_loads(big_yaml)
557+
558+no_files_yaml = \
559+ """
560+results:
561+ result-map:
562+status: completed
563+timing:
564+ completed: 2015-05-21 09:57:03 -0400 EDT
565+ enqueued: 2015-05-21 09:56:59 -0400 EDT
566+ started: 2015-05-21 09:57:02 -0400 EDT
567+"""
568+
569+no_files_obj = yaml_loads(no_files_yaml)
570+
571+
572+class TestCheckForExtraBackup(TestCase):
573+
574+ def test_not_found(self):
575+ try:
576+ # log2 should not be found, and thus no exception.
577+ check_for_extra_backup("log2", good_obj)
578+ except Exception as e:
579+ self.fail("unexpected exception: %s" % e.msg)
580+
581+ def test_find_extra(self):
582+ with self.assertRaises(LogRotateError):
583+ # log1 should be found, and thus cause an exception.
584+ check_for_extra_backup("log1", good_obj)
585+
586+
587+class TestCheckBackup(TestCase):
588+
589+ def test_exists(self):
590+ try:
591+ # log1 should be found, and thus no exception.
592+ check_expected_backup("log1", "unit-fill-logs-0", good_obj)
593+ except Exception as e:
594+ self.fail("unexpected exception: %s" % e.msg)
595+
596+ def test_not_found(self):
597+ with self.assertRaises(LogRotateError):
598+ # log2 should not be found, and thus cause an exception.
599+ check_expected_backup("log2", "unit-fill-logs-0", good_obj)
600+
601+ def test_too_big(self):
602+ with self.assertRaises(LogRotateError):
603+ # log1 is too big, and thus should cause an exception.
604+ check_expected_backup("log1", "unit-fill-logs-0", big_obj)
605+
606+ def test_bad_timestamp(self):
607+ with self.assertRaises(LogRotateError):
608+ # log2 has an invalid timestamp, and thus should cause an
609+ # exception.
610+ check_expected_backup("log2", "unit-fill-logs-0", big_obj)
611+
612+ def test_bad_name(self):
613+ with self.assertRaises(LogRotateError):
614+ # log3 has a completely invalid name, and thus should cause an
615+ # exception.
616+ check_expected_backup("log3", "unit-fill-logs-0", big_obj)
617+
618+
619+class TestCheckLog0(TestCase):
620+
621+ def test_exists(self):
622+ try:
623+ # log0 should be found, and thus no exception.
624+ check_log0("/var/log/juju/unit-fill-logs-0.log", good_obj)
625+ except Exception as e:
626+ self.fail("unexpected exception: %s" % e.msg)
627+
628+ def test_not_found(self):
629+ with self.assertRaises(AttributeError):
630+ # There's no value under result-map, which causes the yaml parser
631+ # to consider it None, and thus it'll cause an AttributeError
632+ check_log0("/var/log/juju/unit-fill-logs-0.log", no_files_obj)
633+
634+ def test_too_big(self):
635+ with self.assertRaises(LogRotateError):
636+ # log0 is too big, and thus should cause an exception.
637+ check_log0(
638+ "/var/log/juju/unit-fill-logs-0.log", big_obj)
639+
640+
641+class TestParseArgs(TestCase):
642+
643+ def test_parse_args(self):
644+ args = parse_args(['machine', 'b', 'c', 'd', 'e'])
645+ self.assertEqual(args, Namespace(
646+ agent='machine', juju_path='b', env_name='c', logs='d',
647+ temp_env_name='e', debug=False))
648+
649+ def test_parse_args_debug(self):
650+ args = parse_args(['--debug', 'unit', 'b', 'c', 'd', 'e'])
651+ self.assertEqual(args, Namespace(
652+ agent='unit', juju_path='b', env_name='c', logs='d',
653+ temp_env_name='e', debug=True))
654
655=== modified file 'test_assess_recovery.py'
656--- test_assess_recovery.py 2015-02-11 17:49:42 +0000
657+++ test_assess_recovery.py 2015-06-03 15:41:41 +0000
658@@ -1,68 +1,8 @@
659-from contextlib import contextmanager
660-import os
661-from subprocess import CalledProcessError
662-from textwrap import dedent
663 from unittest import TestCase
664
665-from mock import patch
666-
667 from assess_recovery import (
668- make_client,
669- parse_new_state_server_from_error,
670 parse_args,
671 )
672-from jujupy import _temp_env as temp_env
673-from utility import temp_dir
674-
675-
676-class AssessRecoveryTestCase(TestCase):
677-
678- def test_parse_new_state_server_from_error(self):
679- output = dedent("""
680- Waiting for address
681- Attempting to connect to 10.0.0.202:22
682- Attempting to connect to 1.2.3.4:22
683- The fingerprint for the ECDSA key sent by the remote host is
684- """)
685- error = CalledProcessError(1, ['foo'], output)
686- address = parse_new_state_server_from_error(error)
687- self.assertEqual('1.2.3.4', address)
688-
689-
690-class TestMakeClient(TestCase):
691-
692- @contextmanager
693- def make_client_cxt(self):
694- td = temp_dir()
695- te = temp_env({'environments': {'foo': {
696- 'orig-name': 'foo', 'name': 'foo'}}})
697- with td as juju_path, te, patch('subprocess.Popen',
698- side_effect=ValueError):
699- with patch('subprocess.check_output') as co_mock:
700- co_mock.return_value = '1.18'
701- yield juju_path
702-
703- def test_make_client(self):
704- with self.make_client_cxt() as juju_path:
705- client = make_client(juju_path, False, 'foo', 'bar')
706- self.assertEqual(client.full_path, os.path.join(juju_path, 'juju'))
707- self.assertEqual(client.debug, False)
708- self.assertEqual(client.env.config['orig-name'], 'foo')
709- self.assertEqual(client.env.config['name'], 'bar')
710- self.assertEqual(client.env.environment, 'bar')
711-
712- def test_make_client_debug(self):
713- with self.make_client_cxt() as juju_path:
714- client = make_client(juju_path, True, 'foo', 'bar')
715- self.assertEqual(client.debug, True)
716-
717- def test_make_client_no_temp_env_name(self):
718- with self.make_client_cxt() as juju_path:
719- client = make_client(juju_path, False, 'foo', None)
720- self.assertEqual(client.full_path, os.path.join(juju_path, 'juju'))
721- self.assertEqual(client.env.config['orig-name'], 'foo')
722- self.assertEqual(client.env.config['name'], 'foo')
723- self.assertEqual(client.env.environment, 'foo')
724
725
726 class TestParseArgs(TestCase):
727
728=== modified file 'test_deploy_stack.py'
729--- test_deploy_stack.py 2015-05-28 22:29:53 +0000
730+++ test_deploy_stack.py 2015-06-03 15:41:41 +0000
731@@ -1,7 +1,7 @@
732 from argparse import (
733 ArgumentParser,
734 Namespace,
735- )
736+)
737 from contextlib import contextmanager
738 import json
739 import logging
740@@ -14,7 +14,7 @@
741 from mock import (
742 call,
743 patch,
744- )
745+)
746 import yaml
747
748 from deploy_stack import (
749@@ -50,7 +50,7 @@
750 )
751 from test_jujupy import (
752 assert_juju_call,
753- )
754+)
755 from utility import temp_dir
756
757
758@@ -274,7 +274,7 @@
759 side_effect=subprocess.CalledProcessError('', '')):
760 with patch('subprocess.call', autospec=True) as c_mock:
761 with self.assertRaises(subprocess.CalledProcessError):
762- run_instances(1, 'qux')
763+ run_instances(1, 'qux')
764 c_mock.assert_called_with(['euca-terminate-instances', 'i-foo'])
765
766 def test_assess_juju_run(self):
767@@ -524,7 +524,7 @@
768 ('juju', '--show-log', 'status', '-e', 'foo'): status,
769 ('juju', '--show-log', 'ssh', '-e', 'foo', 'dummy-sink/0',
770 GET_TOKEN_SCRIPT): 'fake-token',
771- }
772+ }
773 return output[args]
774
775 with patch('subprocess.check_output', side_effect=output,
776@@ -582,7 +582,7 @@
777 cls.RUN_UNAME: juju_run_out,
778 cls.VERSION: '1.38',
779 cls.GET_ENV: 'testing'
780- }
781+ }
782 return output[args]
783
784 @contextmanager
785@@ -590,11 +590,11 @@
786 with patch('subprocess.check_output', side_effect=self.upgrade_output,
787 autospec=True) as co_mock:
788 with patch('subprocess.check_call', autospec=True) as cc_mock:
789- with patch('deploy_stack.check_token', autospec=True):
790- with patch('deploy_stack.get_random_string',
791- return_value="FAKETOKEN", autospec=True):
792- with patch('sys.stdout', autospec=True):
793- yield (co_mock, cc_mock)
794+ with patch('deploy_stack.check_token', autospec=True):
795+ with patch('deploy_stack.get_random_string',
796+ return_value="FAKETOKEN", autospec=True):
797+ with patch('sys.stdout', autospec=True):
798+ yield (co_mock, cc_mock)
799
800 def test_assess_upgrade(self):
801 env = SimpleEnvironment('foo', {'type': 'foo'})
802@@ -643,7 +643,7 @@
803 status = yaml.dump({
804 'machines': {0: {'agent-version': '1.18.17'}},
805 'services': {},
806- })
807+ })
808
809 def test_prepare_environment(self):
810 client = self.get_client()
811@@ -699,7 +699,7 @@
812 client = EnvJujuClient(SimpleEnvironment(
813 'foo', {'type': 'paas'}), '1.23', 'path')
814 self.addContext(patch('deploy_stack.get_machine_dns_name',
815- return_value='foo'))
816+ return_value='foo'))
817 c_mock = self.addContext(patch('subprocess.call'))
818 with boot_context('bar', client, None, [], None, None, None, None,
819 keep_env=False, upload_tools=False):
820@@ -718,7 +718,7 @@
821 client = EnvJujuClient(SimpleEnvironment(
822 'foo', {'type': 'paas'}), '1.23', 'path')
823 self.addContext(patch('deploy_stack.get_machine_dns_name',
824- return_value='foo'))
825+ return_value='foo'))
826 c_mock = self.addContext(patch('subprocess.call'))
827 with boot_context('bar', client, None, [], None, None, None, None,
828 keep_env=True, upload_tools=False):
829@@ -735,7 +735,7 @@
830 client = EnvJujuClient(SimpleEnvironment(
831 'foo', {'type': 'paas'}), '1.23', 'path')
832 self.addContext(patch('deploy_stack.get_machine_dns_name',
833- return_value='foo'))
834+ return_value='foo'))
835 self.addContext(patch('subprocess.call'))
836 with boot_context('bar', client, None, [], None, None, None, None,
837 keep_env=False, upload_tools=True):
838@@ -749,7 +749,7 @@
839 client = EnvJujuClient(SimpleEnvironment(
840 'foo', {'type': 'paas'}), '1.23', 'path')
841 self.addContext(patch('deploy_stack.get_machine_dns_name',
842- return_value='foo'))
843+ return_value='foo'))
844 self.addContext(patch('subprocess.call'))
845 ue_mock = self.addContext(
846 patch('deploy_stack.update_env', wraps=update_env))
847@@ -783,7 +783,7 @@
848 upgrade=False,
849 verbose=False,
850 upload_tools=False,
851- ))
852+ ))
853
854 def test_upload_tools(self):
855 args = deploy_job_parse_args(['foo', 'bar', 'baz', '--upload-tools'])
856
857=== modified file 'test_jujupy.py'
858--- test_jujupy.py 2015-06-01 21:06:33 +0000
859+++ test_jujupy.py 2015-06-03 15:41:41 +0000
860@@ -4,7 +4,7 @@
861 from datetime import (
862 datetime,
863 timedelta,
864- )
865+)
866 import os
867 import shutil
868 import StringIO
869@@ -24,11 +24,12 @@
870 get_environments_path,
871 get_jenv_path,
872 NoSuchEnvironment,
873- )
874+)
875 from jujupy import (
876 CannotConnectEnv,
877 Environment,
878 EnvJujuClient,
879+ EnvJujuClient22,
880 EnvJujuClient24,
881 EnvJujuClient25,
882 ErroredUnit,
883@@ -41,11 +42,13 @@
884 temp_bootstrap_env,
885 _temp_env as temp_env,
886 uniquify_local,
887+ make_client,
888+ parse_new_state_server_from_error,
889 )
890 from utility import (
891 scoped_environ,
892 temp_dir,
893- )
894+)
895
896
897 def assert_juju_call(test_case, mock_method, client, expected_args,
898@@ -98,7 +101,8 @@
899 SimpleEnvironment('baz', {'type': 'cloudsigma'}),
900 '1.25-foobar', 'path')
901 env = client._shell_environ()
902- self.assertEqual(env[JUJU_DEV_FEATURE_FLAGS], 'cloudsigma')
903+ "".split()
904+ self.assertTrue('cloudsigma' in env[JUJU_DEV_FEATURE_FLAGS].split(","))
905
906 def test__shell_environ_juju_home(self):
907 client = self.client_class(
908@@ -107,6 +111,23 @@
909 self.assertEqual(env['JUJU_HOME'], 'asdf')
910
911
912+class TestEnvJujuClient22(ClientTest):
913+
914+ client_class = EnvJujuClient22
915+
916+ def test__shell_environ(self):
917+ client = self.client_class(
918+ SimpleEnvironment('baz', {'type': 'ec2'}), '1.22-foobar', 'path')
919+ env = client._shell_environ()
920+ self.assertEqual(env.get(JUJU_DEV_FEATURE_FLAGS), 'actions')
921+
922+ def test__shell_environ_juju_home(self):
923+ client = self.client_class(
924+ SimpleEnvironment('baz', {'type': 'ec2'}), '1.22-foobar', 'path')
925+ env = client._shell_environ(juju_home='asdf')
926+ self.assertEqual(env['JUJU_HOME'], 'asdf')
927+
928+
929 class TestEnvJujuClient24(TestEnvJujuClient25):
930
931 client_class = EnvJujuClient25
932@@ -165,6 +186,7 @@
933 yield '1.16'
934 yield '1.16.1'
935 yield '1.15'
936+ yield '1.22.1'
937 yield '1.24-alpha1'
938 yield '1.24.7'
939 yield '1.25.1'
940@@ -185,6 +207,8 @@
941 self.assertIs(EnvJujuClient, type(client))
942 self.assertEqual('1.15', client.version)
943 client = EnvJujuClient.by_version(None)
944+ self.assertIs(type(client), EnvJujuClient22)
945+ client = EnvJujuClient.by_version(None)
946 self.assertIs(type(client), EnvJujuClient24)
947 self.assertEqual(client.version, '1.24-alpha1')
948 client = EnvJujuClient.by_version(None)
949@@ -231,7 +255,7 @@
950 client = EnvJujuClient(env, None, 'my/juju/bin')
951 full = client._full_args('action bar', False, ('baz', 'qux'))
952 self.assertEqual((
953- 'juju', '--show-log', 'action', 'bar', '-e', 'foo', 'baz', 'qux'),
954+ 'juju', '--show-log', 'action', 'bar', '-e', 'foo', 'baz', 'qux', ),
955 full)
956
957 def test_bootstrap_hpcloud(self):
958@@ -577,9 +601,9 @@
959 '0': {'state-server-member-status': 'has-vote'},
960 '1': {'state-server-member-status': 'has-vote'},
961 '2': {'state-server-member-status': 'has-vote'},
962- },
963+ },
964 'services': {},
965- })
966+ })
967 client = EnvJujuClient(SimpleEnvironment('local'), None, None)
968 with patch.object(client, 'get_juju_output', return_value=value):
969 client.wait_for_ha()
970@@ -590,9 +614,9 @@
971 '0': {'state-server-member-status': 'no-vote'},
972 '1': {'state-server-member-status': 'no-vote'},
973 '2': {'state-server-member-status': 'no-vote'},
974- },
975+ },
976 'services': {},
977- })
978+ })
979 client = EnvJujuClient(SimpleEnvironment('local'), None, None)
980 with patch('sys.stdout'):
981 with patch.object(client, 'get_juju_output', return_value=value):
982@@ -606,9 +630,9 @@
983 'machines': {
984 '0': {'state-server-member-status': 'has-vote'},
985 '1': {'state-server-member-status': 'has-vote'},
986- },
987+ },
988 'services': {},
989- })
990+ })
991 client = EnvJujuClient(SimpleEnvironment('local'), None, None)
992 with patch('jujupy.until_timeout', lambda x: range(0)):
993 with patch.object(client, 'get_juju_output', return_value=value):
994@@ -621,7 +645,7 @@
995 value = yaml.safe_dump({
996 'machines': {
997 '0': {'agent-state': 'started'},
998- },
999+ },
1000 'services': {
1001 'jenkins': {
1002 'units': {
1003@@ -638,9 +662,9 @@
1004 value = yaml.safe_dump({
1005 'machines': {
1006 '0': {'agent-state': 'started'},
1007- },
1008+ },
1009 'services': {},
1010- })
1011+ })
1012 client = EnvJujuClient(SimpleEnvironment('local'), None, None)
1013 with patch('jujupy.until_timeout', lambda x: range(0)):
1014 with patch.object(client, 'get_juju_output', return_value=value):
1015@@ -981,7 +1005,7 @@
1016 'root-dir': get_local_root(fake_home, client.env),
1017 'agent-version': agent_version,
1018 'test-mode': True,
1019- }}})
1020+ }}})
1021 stub_bootstrap()
1022
1023 def test_temp_bootstrap_env_provides_dir(self):
1024@@ -1052,7 +1076,7 @@
1025 self.assertEqual(mock_cfds.mock_calls, [
1026 call(os.path.join(fake_home, 'qux'), 8000000, 'MongoDB files'),
1027 call('/var/lib/lxc', 2000000, 'LXC containers'),
1028- ])
1029+ ])
1030
1031 def test_check_space_local_kvm(self):
1032 env = SimpleEnvironment('qux', {'type': 'local', 'container': 'kvm'})
1033@@ -1064,7 +1088,7 @@
1034 self.assertEqual(mock_cfds.mock_calls, [
1035 call(os.path.join(fake_home, 'qux'), 8000000, 'MongoDB files'),
1036 call('/var/lib/uvtool/libvirt/images', 2000000, 'KVM disk files'),
1037- ])
1038+ ])
1039
1040 def test_error_on_jenv(self):
1041 env = SimpleEnvironment('qux', {'type': 'local'})
1042@@ -1337,7 +1361,7 @@
1043 self.assertEqual(list(status.iter_machines(containers=True)), [
1044 ('1', status.status['machines']['1']),
1045 ('1/lxc/0', {'baz': 'qux'}),
1046- ])
1047+ ])
1048
1049 def test_agent_items_empty(self):
1050 status = Status({'machines': {}, 'services': {}}, '')
1051@@ -1483,7 +1507,7 @@
1052 status.get_subordinate_units('dummy-sink'), [
1053 {'chaos-monkey/1': {'agent-state': 'started'}},
1054 {'chaos-monkey/2': {'agent-state': 'started'}}]
1055- )
1056+ )
1057 with self.assertRaisesRegexp(KeyError, 'foo'):
1058 status.get_subordinate_units('foo')
1059
1060@@ -1593,7 +1617,7 @@
1061 'machines': {'0': {
1062 'agent-state-info': failure}},
1063 'services': {},
1064- }, '')
1065+ }, '')
1066 with self.assertRaises(ErroredUnit) as e_cxt:
1067 status.check_agents_started()
1068 e = e_cxt.exception
1069@@ -1653,14 +1677,14 @@
1070 old_status = Status({
1071 'machines': {
1072 'bar': 'bar_info',
1073- }
1074- }, '')
1075+ }
1076+ }, '')
1077 new_status = Status({
1078 'machines': {
1079 'foo': 'foo_info',
1080 'bar': 'bar_info',
1081- }
1082- }, '')
1083+ }
1084+ }, '')
1085 self.assertItemsEqual(new_status.iter_new_machines(old_status),
1086 [('foo', 'foo_info')])
1087
1088@@ -1669,8 +1693,8 @@
1089 'machines': {
1090 '0': {'instance-id': 'foo-bar'},
1091 '1': {},
1092- }
1093- }, '')
1094+ }
1095+ }, '')
1096 self.assertEqual(status.get_instance_id('0'), 'foo-bar')
1097 with self.assertRaises(KeyError):
1098 status.get_instance_id('1')
1099@@ -1965,6 +1989,62 @@
1100 mock_get.assert_called_with(env, 'tools-metadata-url')
1101 self.assertEqual(0, mock_set.call_count)
1102
1103+ def test_action_do(self):
1104+ client = EnvJujuClient(SimpleEnvironment(None, {'type': 'local'}),
1105+ '1.23-series-arch', None)
1106+ with patch.object(EnvJujuClient, 'get_juju_output') as mock:
1107+ mock.return_value = \
1108+ "Action queued with id: 5a92ec93-d4be-4399-82dc-7431dbfd08f9"
1109+ id = client.action_do("foo/0", "myaction", "param=5")
1110+ self.assertEqual(id, "5a92ec93-d4be-4399-82dc-7431dbfd08f9")
1111+ mock.assert_called_once_with(
1112+ 'action do', 'foo/0', 'myaction', "param=5"
1113+ )
1114+
1115+ def test_action_do_error(self):
1116+ client = EnvJujuClient(SimpleEnvironment(None, {'type': 'local'}),
1117+ '1.23-series-arch', None)
1118+ with patch.object(EnvJujuClient, 'get_juju_output') as mock:
1119+ mock.return_value = "some bad text"
1120+ with self.assertRaisesRegexp(Exception,
1121+ "Action id not found in output"):
1122+ client.action_do("foo/0", "myaction", "param=5")
1123+
1124+ def test_action_fetch(self):
1125+ client = EnvJujuClient(SimpleEnvironment(None, {'type': 'local'}),
1126+ '1.23-series-arch', None)
1127+ with patch.object(EnvJujuClient, 'get_juju_output') as mock:
1128+ ret = "status: completed\nfoo: bar"
1129+ mock.return_value = ret
1130+ out = client.action_fetch("123")
1131+ self.assertEqual(out, ret)
1132+ mock.assert_called_once_with(
1133+ 'action fetch', '123', "--wait", "1m"
1134+ )
1135+
1136+ def test_action_fetch_timeout(self):
1137+ client = EnvJujuClient(SimpleEnvironment(None, {'type': 'local'}),
1138+ '1.23-series-arch', None)
1139+ ret = "status: pending\nfoo: bar"
1140+ with patch.object(EnvJujuClient,
1141+ 'get_juju_output', return_value=ret):
1142+ with self.assertRaisesRegexp(Exception,
1143+ "timed out waiting for action"):
1144+ client.action_fetch("123")
1145+
1146+ def test_action_do_fetch(self):
1147+ client = EnvJujuClient(SimpleEnvironment(None, {'type': 'local'}),
1148+ '1.23-series-arch', None)
1149+ with patch.object(EnvJujuClient, 'get_juju_output') as mock:
1150+ ret = "status: completed\nfoo: bar"
1151+ # setting side_effect to an iterable will return the next value
1152+ # from the list each time the function is called.
1153+ mock.side_effect = [
1154+ "Action queued with id: 5a92ec93-d4be-4399-82dc-7431dbfd08f9",
1155+ ret]
1156+ out = client.action_do_fetch("foo/0", "myaction", "param=5")
1157+ self.assertEqual(out, ret)
1158+
1159
1160 class TestGroupReporter(TestCase):
1161
1162@@ -2099,3 +2179,53 @@
1163 ])
1164 reporter.finish()
1165 self.assertEqual(sio.getvalue(), changes[-1] + "\n")
1166+
1167+
1168+class TestMakeClient(TestCase):
1169+
1170+ @contextmanager
1171+ def make_client_cxt(self):
1172+ td = temp_dir()
1173+ te = temp_env({'environments': {'foo': {
1174+ 'orig-name': 'foo', 'name': 'foo'}}})
1175+ with td as juju_path, te, patch('subprocess.Popen',
1176+ side_effect=ValueError):
1177+ with patch('subprocess.check_output') as co_mock:
1178+ co_mock.return_value = '1.18'
1179+ yield juju_path
1180+
1181+ def test_make_client(self):
1182+ with self.make_client_cxt() as juju_path:
1183+ client = make_client(juju_path, False, 'foo', 'bar')
1184+ self.assertEqual(client.full_path, os.path.join(juju_path, 'juju'))
1185+ self.assertEqual(client.debug, False)
1186+ self.assertEqual(client.env.config['orig-name'], 'foo')
1187+ self.assertEqual(client.env.config['name'], 'bar')
1188+ self.assertEqual(client.env.environment, 'bar')
1189+
1190+ def test_make_client_debug(self):
1191+ with self.make_client_cxt() as juju_path:
1192+ client = make_client(juju_path, True, 'foo', 'bar')
1193+ self.assertEqual(client.debug, True)
1194+
1195+ def test_make_client_no_temp_env_name(self):
1196+ with self.make_client_cxt() as juju_path:
1197+ client = make_client(juju_path, False, 'foo', None)
1198+ self.assertEqual(client.full_path, os.path.join(juju_path, 'juju'))
1199+ self.assertEqual(client.env.config['orig-name'], 'foo')
1200+ self.assertEqual(client.env.config['name'], 'foo')
1201+ self.assertEqual(client.env.environment, 'foo')
1202+
1203+
1204+class AssessParseStateServerFromErrorTestCase(TestCase):
1205+
1206+ def test_parse_new_state_server_from_error(self):
1207+ output = dedent("""
1208+ Waiting for address
1209+ Attempting to connect to 10.0.0.202:22
1210+ Attempting to connect to 1.2.3.4:22
1211+ The fingerprint for the ECDSA key sent by the remote host is
1212+ """)
1213+ error = subprocess.CalledProcessError(1, ['foo'], output)
1214+ address = parse_new_state_server_from_error(error)
1215+ self.assertEqual('1.2.3.4', address)

Subscribers

People subscribed via source and target branches