Merge lp:~thumper/python-jujuclient/jes-cache-file into lp:python-jujuclient

Proposed by Tim Penhey
Status: Merged
Merged at revision: 61
Proposed branch: lp:~thumper/python-jujuclient/jes-cache-file
Merge into: lp:python-jujuclient
Diff against target: 202 lines (+137/-4)
3 files modified
.bzrignore (+1/-0)
jujuclient.py (+35/-3)
test_jujuclient.py (+101/-1)
To merge this branch: bzr merge lp:~thumper/python-jujuclient/jes-cache-file
Reviewer Review Type Date Requested Status
Tim Van Steenburgh (community) Approve
Review via email: mp+265085@code.launchpad.net

This proposal supersedes a proposal from 2015-06-10.

Commit message

Teach python-jujuclient about the cache.yaml file for cached environment information.

Description of the change

With the jes feature flag, we now don't store environment details in .jenv files, but in a single cache.yaml file.

This branch teaches python-jujuclient how to get the required information out of it.

Tests all pass.

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote : Posted in a previous version of this proposal

I have not been able to run the tests for some reason, and I am unfamiliar with tox:

$ JUJU_TEST_ENV="mary2" tox -e py27
Traceback (most recent call last):
  File "/usr/bin/tox", line 9, in <module>
    load_entry_point('tox==1.6.0', 'console_scripts', 'tox')()
  File "/usr/lib/python2.7/dist-packages/tox/_cmdline.py", line 25, in main
    config = parseconfig(args, 'tox')
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 44, in parseconfig
    parseini(config, inipath)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 236, in __init__
    config)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 292, in _makeenvconfig
    vc.setenv = reader.getdict(section, 'setenv')
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 419, in getdict
    s = self.getdefault(section, name, None)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 511, in getdefault
    x = self._replace(x)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 618, in _replace
    return rexpattern.sub(replace_func, x)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 609, in _replace_match_no_quote
    return self._replace_match(match, quote=False)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 604, in _replace_match
    return handler(match, quote)
  File "/usr/lib/python2.7/dist-packages/tox/_config.py", line 541, in _replace_env
    (envkey, envkey))
tox.ConfigError: ConfigError: substitution env:'JUJU_TEST_ENV:"test"': unkown environment variable 'JUJU_TEST_ENV:"test"'

Revision history for this message
Kapil Thangavelu (hazmat) wrote : Posted in a previous version of this proposal

talked with tim on irc, think he worked through this

Revision history for this message
Tim Penhey (thumper) wrote : Posted in a previous version of this proposal

Managed to get the tests running, but they fail, even for trunk.

https://bugs.launchpad.net/python-jujuclient/+bug/1465084

Revision history for this message
Tim Van Steenburgh (tvansteenburgh) wrote :

LGTM, thanks thumper.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-07-09 20:46:45 +0000
3+++ .bzrignore 2015-07-17 05:40:13 +0000
4@@ -13,3 +13,4 @@
5 .tox
6 .coverage
7 juju-backup-20150709-202028.tar.gz
8+__pycache__
9
10=== modified file 'jujuclient.py'
11--- jujuclient.py 2015-07-16 22:19:20 +0000
12+++ jujuclient.py 2015-07-17 05:40:13 +0000
13@@ -207,16 +207,48 @@
14 return s
15
16 def parse_env(self, env_name):
17- import yaml
18 jhome = os.path.expanduser(
19 os.environ.get('JUJU_HOME', '~/.juju'))
20+
21+ # Look in the cache file first.
22+ cache_file = os.path.join(jhome, 'environments', 'cache.yaml')
23 jenv = os.path.join(jhome, 'environments', '%s.jenv' % env_name)
24+
25+ if os.path.exists(cache_file):
26+ try:
27+ return jhome, self.environment_from_cache(env_name, cache_file)
28+ except EnvironmentNotBootstrapped:
29+ pass
30+ # Fall through to getting the info from the jenv
31 if not os.path.exists(jenv):
32 raise EnvironmentNotBootstrapped(env_name)
33-
34+ return jhome, self.environment_from_jenv(jenv)
35+
36+ def environment_from_cache(self, env_name, cache_filename):
37+ import yaml
38+ with open(cache_filename) as fh:
39+ data = yaml.safe_load(fh.read())
40+ try:
41+ # environment holds:
42+ # user, env-uuid, server-uuid
43+ environment = data['environment'][env_name]
44+ server = data['server-data'][environment['server-uuid']]
45+ return {
46+ 'user': environment['user'],
47+ 'password': server['identities'][environment['user']],
48+ 'environ-uuid': environment['env-uuid'],
49+ 'server-uuid': environment['server-uuid'],
50+ 'state-servers': server['api-endpoints'],
51+ 'ca-cert': server['ca-cert'],
52+ }
53+ except KeyError:
54+ raise EnvironmentNotBootstrapped(env_name)
55+
56+ def environment_from_jenv(self, jenv):
57+ import yaml
58 with open(jenv) as fh:
59 data = yaml.safe_load(fh.read())
60- return jhome, data
61+ return data
62
63 @staticmethod
64 def split_host_port(server):
65
66=== modified file 'test_jujuclient.py'
67--- test_jujuclient.py 2015-07-16 22:19:20 +0000
68+++ test_jujuclient.py 2015-07-17 05:40:13 +0000
69@@ -1,3 +1,4 @@
70+import copy
71 import errno
72 import mock
73 import os
74@@ -15,7 +16,10 @@
75 # Watch wrappers
76 WaitForNoMachines,
77 # Facades
78- Actions, Annotations, Backups, Charms, HA, KeyManager, UserManager)
79+ Actions, Annotations, Backups, Charms, HA, KeyManager, UserManager,
80+ # Exceptions
81+ EnvironmentNotBootstrapped,
82+)
83
84
85 ENV_NAME = os.environ.get("JUJU_TEST_ENV")
86@@ -24,6 +28,16 @@
87 raise ValueError("No Testing Environment Defined.")
88
89
90+SAMPLE_CONFIG = {
91+ 'user': 'tester',
92+ 'password': 'sekrit',
93+ 'environ-uuid': 'some-uuid',
94+ 'server-uuid': 'server-uuid',
95+ 'state-servers': ['localhost:12345'],
96+ 'ca-cert': 'test-cert',
97+}
98+
99+
100 def reset(env):
101 status = env.status()
102 env.destroy_machines(
103@@ -57,6 +71,11 @@
104
105 class ClientConnectorTest(unittest.TestCase):
106
107+ def mkdir(self):
108+ d = tempfile.mkdtemp()
109+ self.addCleanup(shutil.rmtree, d)
110+ return d
111+
112 @mock.patch('jujuclient.websocket')
113 def test_connect_socket(self, websocket):
114 address = "wss://abc:17070"
115@@ -104,6 +123,87 @@
116 ValueError, Connector.split_host_port, 'I am not an ip/port combo'
117 )
118
119+ def test_parse_env_missing(self):
120+ temp_juju_home = self.mkdir()
121+ with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
122+ connector = Connector()
123+ self.assertRaises(
124+ EnvironmentNotBootstrapped,
125+ connector.parse_env,
126+ 'missing')
127+
128+ def test_parse_env_jenv(self):
129+ temp_juju_home = self.mkdir()
130+ self.write_jenv(temp_juju_home, 'test-env', SAMPLE_CONFIG)
131+ with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
132+ connector = Connector()
133+ home, env = connector.parse_env('test-env')
134+ self.assertEqual(home, temp_juju_home)
135+ self.assertEqual(env, SAMPLE_CONFIG)
136+
137+ def test_parse_cache_file(self):
138+ temp_juju_home = self.mkdir()
139+ self.write_cache_file(temp_juju_home, 'test-env', SAMPLE_CONFIG)
140+ with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
141+ connector = Connector()
142+ home, env = connector.parse_env('test-env')
143+ self.assertEqual(home, temp_juju_home)
144+ self.assertEqual(env, SAMPLE_CONFIG)
145+
146+ def test_parse_cache_file_missing_env(self):
147+ """Create a valid cache file, but look for an environment that isn't there.
148+ """
149+ temp_juju_home = self.mkdir()
150+ self.write_cache_file(temp_juju_home, 'test-env', SAMPLE_CONFIG)
151+ with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
152+ connector = Connector()
153+ self.assertRaises(
154+ EnvironmentNotBootstrapped,
155+ connector.parse_env,
156+ 'missing')
157+
158+ def test_parse_env_cache_file_first(self):
159+ """The cache file has priority over a jenv file."""
160+ temp_juju_home = self.mkdir()
161+ content = copy.deepcopy(SAMPLE_CONFIG)
162+ self.write_jenv(temp_juju_home, 'test-env', content)
163+ # Now change the password.
164+ content['password'] = 'new password'
165+ self.write_cache_file(temp_juju_home, 'test-env', content)
166+ with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
167+ connector = Connector()
168+ home, env = connector.parse_env('test-env')
169+ self.assertEqual(home, temp_juju_home)
170+ self.assertEqual(env, content)
171+
172+ def write_jenv(self, juju_home, env_name, content):
173+ env_dir = os.path.join(juju_home, 'environments')
174+ if not os.path.exists(env_dir):
175+ os.mkdir(env_dir)
176+ jenv = os.path.join(env_dir, '%s.jenv' % env_name)
177+ with open(jenv, 'w') as f:
178+ yaml.dump(content, f, default_flow_style=False)
179+
180+ def write_cache_file(self, juju_home, env_name, content):
181+ env_dir = os.path.join(juju_home, 'environments')
182+ if not os.path.exists(env_dir):
183+ os.mkdir(env_dir)
184+ filename = os.path.join(env_dir, 'cache.yaml')
185+ cache_content = {
186+ 'environment': {
187+ env_name: {'env-uuid': content['environ-uuid'],
188+ 'server-uuid': content['server-uuid'],
189+ 'user': content['user']}},
190+ 'server-data': {
191+ content['server-uuid']: {
192+ 'api-endpoints': content['state-servers'],
193+ 'ca-cert': content['ca-cert'],
194+ 'identities': {content['user']: content['password']}}},
195+ # Explicitly don't care about 'server-user' here.
196+ }
197+ with open(filename, 'w') as f:
198+ yaml.dump(cache_content, f, default_flow_style=False)
199+
200
201 class KeyManagerTest(unittest.TestCase):
202 def setUp(self):

Subscribers

People subscribed via source and target branches

to all changes: