Merge lp:~ev/charms/xenial/cassandra/snap into lp:~cassandra-charmers/charms/xenial/cassandra/trunk
- Xenial Xerus (16.04)
- snap
- Merge into trunk
Status: | Merged |
---|---|
Merge reported by: | Stuart Bishop |
Merged at revision: | not available |
Proposed branch: | lp:~ev/charms/xenial/cassandra/snap |
Merge into: | lp:~cassandra-charmers/charms/xenial/cassandra/trunk |
Diff against target: |
498 lines (+224/-31) 4 files modified
config.yaml (+4/-4) hooks/actions.py (+10/-2) hooks/helpers.py (+167/-24) tests/test_helpers.py (+43/-1) |
To merge this branch: | bzr merge lp:~ev/charms/xenial/cassandra/snap |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Review via email: mp+297723@code.launchpad.net |
Commit message
Description of the change
Stuart Bishop (stub) wrote : | # |
Thanks for the thorough review! I've fixed the raised issues and mentioned the individual revnos inline.
Stuart Bishop (stub) wrote : | # |
This is all looking good. A few comments inline, but nothing series except for the encrypt() method failing under trusty.
We will want integration tests, which starts with creating TestSnapDeploym
The integration tests at the moment will almost certainly fail for snap, as they 'juju run --unit ... nodetool ...' so we either need the charm to create symlinks in /usr/local/bin so the standard tool names work, or update the callsites in the test suite (just 3 or 4 I see). Ideally the snap would use the standard tool names like 'nodetool' rather than 'cassandra.
- 390. By Evan
-
Add bug reference.
- 391. By Evan
-
Correct default for edition.
- 392. By Evan
-
Prefer platform.dist to host.lsb_release
- 393. By Evan
-
Do not try to decode from a string
- 394. By Evan
-
Fix tests; reset log mocks between calls.
- 395. By Evan
-
Revert move from lsb_release to platform: https:/
/bugs.python. org/issue1322# msg263313
Stuart Bishop (stub) wrote : | # |
I think this is past the point where it can land. I'm running tests.
Stuart Bishop (stub) wrote : | # |
I needed to make some more changes, which at at lp:~stub/charms/trusty/cassandra/xenial. The charm is now multi-series, and I'm pushing it to a new home at https:/
There are no integration tests of the snap install yet. Which is fine, as there is no documentation for the new feature either :-) We should fix both of these issues on the git branch.
Preview Diff
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2016-02-26 16:47:03 +0000 |
3 | +++ config.yaml 2016-06-29 15:55:10 +0000 |
4 | @@ -74,16 +74,16 @@ |
5 | type: string |
6 | default: community |
7 | description: > |
8 | - One of 'community' or 'dse'. 'community' uses the |
9 | - Apache Cassandra packages. 'dse' is for DataStax |
10 | - Enterprise. Selecting 'dse' overrides the jvm setting. |
11 | + One of 'community', 'dse', or 'apache-snap'. 'community' uses the |
12 | + Apache Cassandra packages. 'dse' is for DataStax Enterprise. |
13 | + Selecting 'dse' overrides the jvm setting. 'apache-snap' uses a |
14 | + snap package of Apache Cassandra. |
15 | jre: |
16 | type: string |
17 | default: openjdk |
18 | description: > |
19 | Which Java runtime environment to use. May be 'openjdk' or |
20 | 'oracle'. |
21 | - |
22 | # Storage configuration |
23 | wait_for_storage_broker: |
24 | type: boolean |
25 | |
26 | === modified file 'hooks/actions.py' |
27 | --- hooks/actions.py 2016-06-09 10:59:24 +0000 |
28 | +++ hooks/actions.py 2016-06-29 15:55:10 +0000 |
29 | @@ -277,7 +277,9 @@ |
30 | @action |
31 | def install_cassandra_packages(): |
32 | helpers.install_packages(helpers.get_cassandra_packages()) |
33 | - if helpers.get_jre() != 'oracle': |
34 | + if helpers.get_cassandra_edition() == 'apache-snap': |
35 | + helpers.install_cassandra_snap() |
36 | + elif helpers.get_jre() != 'oracle': |
37 | subprocess.check_call(['update-java-alternatives', |
38 | '--jre-headless', |
39 | '--set', 'java-1.8.0-openjdk-amd64']) |
40 | @@ -286,6 +288,7 @@ |
41 | @action |
42 | def ensure_cassandra_package_status(): |
43 | helpers.ensure_package_status(helpers.get_cassandra_packages()) |
44 | + helpers.ensure_cassandra_snap_installed() |
45 | |
46 | |
47 | def _fetch_oracle_jre(): |
48 | @@ -361,6 +364,8 @@ |
49 | def install_oracle_jre(): |
50 | if helpers.get_jre() != 'oracle': |
51 | return |
52 | + if helpers.get_cassandra_edition() == 'apache-snap': |
53 | + return |
54 | |
55 | tarball = _fetch_oracle_jre() |
56 | _install_oracle_jre_tarball(tarball) |
57 | @@ -368,6 +373,9 @@ |
58 | |
59 | @action |
60 | def emit_java_version(): |
61 | + if helpers.get_cassandra_edition() == 'apache-snap': |
62 | + return |
63 | + |
64 | # Log the version for posterity. Could be useful since Oracle JRE |
65 | # security updates are not automated. |
66 | version = subprocess.check_output(['java', '-version'], |
67 | @@ -426,7 +434,7 @@ |
68 | rack={} |
69 | ''').format(datacenter, rack) |
70 | rackdc_path = helpers.get_cassandra_rackdc_file() |
71 | - host.write_file(rackdc_path, rackdc_properties.encode('UTF-8')) |
72 | + helpers.write_config(rackdc_path, rackdc_properties) |
73 | |
74 | |
75 | def needs_reset_auth_keyspace_replication(): |
76 | |
77 | === modified file 'hooks/helpers.py' |
78 | --- hooks/helpers.py 2016-06-09 10:59:24 +0000 |
79 | +++ hooks/helpers.py 2016-06-29 15:55:10 +0000 |
80 | @@ -125,6 +125,46 @@ |
81 | dpkg.communicate(input=''.join(selections).encode('US-ASCII')) |
82 | |
83 | |
84 | +# FOR CHARMHELPERS |
85 | +@logged |
86 | +def install_cassandra_snap(): |
87 | + '''Ensure that the Cassandra snap is installed at the current revision. |
88 | + ''' |
89 | + cmd = ['snap', 'list', 'cassandra'] |
90 | + output = subprocess.check_output(cmd, universal_newlines=True) |
91 | + if 'cassandra' not in output: |
92 | + with autostart_disabled(['snap.cassandra.cassandra']): |
93 | + subprocess.check_call(['snap', 'install', 'cassandra']) |
94 | + else: |
95 | + # snap will exit 1 if the snap is already the newest version. |
96 | + # Filed as http://pad.lv/1595064 |
97 | + subprocess.call(['snap', 'refresh', 'cassandra']) |
98 | + |
99 | + plug = 'cassandra:mount-observe' |
100 | + slot = 'ubuntu-core:mount-observe' |
101 | + subprocess.check_call(['snap', 'connect', plug, slot]) |
102 | + |
103 | + |
104 | +# FOR CHARMHELPERS |
105 | +@logged |
106 | +def ensure_cassandra_snap_installed(): |
107 | + '''Check if the cassandra snap is installed and raise RuntimeError if it is |
108 | + not. |
109 | + ''' |
110 | + cmd = ['snap', 'list', 'cassandra'] |
111 | + output = subprocess.check_output(cmd, universal_newlines=True) |
112 | + if 'cassandra' not in output: |
113 | + raise RuntimeError('Cassandra snap not installed.') |
114 | + |
115 | + |
116 | +def get_snap_env(envar): |
117 | + '''Return the value of an environment variable present in the snap wrapper |
118 | + scripts. |
119 | + ''' |
120 | + cmd = ['/snap/bin/cassandra.env-get', envar] |
121 | + return subprocess.check_output(cmd, universal_newlines=True).strip('\n') |
122 | + |
123 | + |
124 | def get_seed_ips(): |
125 | '''Return the set of seed ip addresses. |
126 | |
127 | @@ -149,7 +189,9 @@ |
128 | ''' |
129 | import relations |
130 | storage = relations.StorageRelation() |
131 | - if storage.mountpoint: |
132 | + if get_cassandra_edition() == 'apache-snap': |
133 | + root = get_snap_env('SNAP_DATA') |
134 | + elif storage.mountpoint: |
135 | root = os.path.join(storage.mountpoint, 'cassandra') |
136 | else: |
137 | root = '/var/lib/cassandra' |
138 | @@ -169,10 +211,13 @@ |
139 | component = os.sep |
140 | for p in absdir.split(os.sep)[1:-1]: |
141 | component = os.path.join(component, p) |
142 | - if not os.path.exists(p): |
143 | + if not os.path.exists(component): |
144 | host.mkdir(component) |
145 | assert component == os.path.split(absdir)[0] |
146 | - host.mkdir(absdir, owner='cassandra', group='cassandra', perms=0o750) |
147 | + if get_cassandra_edition() == 'apache-snap': |
148 | + host.mkdir(absdir, owner='root', group='root', perms=0o750) |
149 | + else: |
150 | + host.mkdir(absdir, owner='cassandra', group='cassandra', perms=0o750) |
151 | return absdir |
152 | |
153 | |
154 | @@ -277,6 +322,15 @@ |
155 | host.write_file(backup_path, f.read(), perms=0o600) |
156 | |
157 | |
158 | +def get_snap_version(snap): |
159 | + '''Get the version string for an installed snap.''' |
160 | + cmd = ['snap', 'list', snap] |
161 | + out = subprocess.check_output(cmd, universal_newlines=True) |
162 | + match = re.search('\n{}\s*(\S*)'.format(snap), out) |
163 | + if match: |
164 | + return match.groups(0)[0] |
165 | + |
166 | + |
167 | # FOR CHARMHELPERS |
168 | def get_package_version(package): |
169 | cache = fetch.apt_cache() |
170 | @@ -305,22 +359,34 @@ |
171 | def get_cassandra_edition(): |
172 | config = hookenv.config() |
173 | edition = config['edition'].lower() |
174 | - if edition not in ('community', 'dse'): |
175 | + if edition not in ('community', 'dse', 'apache-snap'): |
176 | hookenv.log('Unknown edition {!r}. Using community.'.format(edition), |
177 | ERROR) |
178 | edition = 'community' |
179 | + release = host.lsb_release()['DISTRIB_CODENAME'] |
180 | + if edition == 'apache-snap' and release in ['precise', 'trusty']: |
181 | + msg = '{!r} cannot be used with {!r}. Using community.' |
182 | + msg = msg.format(release, edition) |
183 | + hookenv.log(msg, ERROR) |
184 | + edition = 'community' |
185 | + |
186 | return edition |
187 | |
188 | |
189 | def get_cassandra_service(): |
190 | '''Cassandra upstart service''' |
191 | - if get_cassandra_edition() == 'dse': |
192 | + if get_cassandra_edition() == 'apache-snap': |
193 | + return 'snap.cassandra.cassandra' |
194 | + elif get_cassandra_edition() == 'dse': |
195 | return 'dse' |
196 | return 'cassandra' |
197 | |
198 | |
199 | def get_cassandra_version(): |
200 | - if get_cassandra_edition() == 'dse': |
201 | + edition = get_cassandra_edition() |
202 | + if edition == 'apache-snap': |
203 | + return get_snap_version('cassandra') |
204 | + elif edition == 'dse': |
205 | dse_ver = get_package_version('dse-full') |
206 | if not dse_ver: |
207 | return None |
208 | @@ -338,12 +404,37 @@ |
209 | |
210 | |
211 | def get_cassandra_config_dir(): |
212 | - if get_cassandra_edition() == 'dse': |
213 | + edition = get_cassandra_edition() |
214 | + if edition == 'apache-snap': |
215 | + return get_snap_env('CASSANDRA_CONF') |
216 | + elif edition == 'dse': |
217 | return '/etc/dse/cassandra' |
218 | else: |
219 | return '/etc/cassandra' |
220 | |
221 | |
222 | +def get_snap_config_file(filename): |
223 | + '''Get the contents of the named configuration file from the current snap |
224 | + data directory. |
225 | + ''' |
226 | + cmd = ['/snap/bin/cassandra.config-get', filename] |
227 | + return subprocess.check_output(cmd, universal_newlines=True) |
228 | + |
229 | + |
230 | +def set_snap_config_file(filename, contents): |
231 | + '''Install a new copy of the configuration file with the provided contents |
232 | + in the current snap data directory. |
233 | + ''' |
234 | + cmd = ['/snap/bin/cassandra.config-set', filename] |
235 | + cs = subprocess.Popen(cmd, stdin=subprocess.PIPE, universal_newlines=True) |
236 | + _, err = cs.communicate(input=contents) |
237 | + if err: |
238 | + hookenv.log('Error calling {}:\n{}'.format(' '.join(cmd), err)) |
239 | + if cs.returncode != 0: |
240 | + msg = '{} exited with code {}'.format(' '.join(cmd), cs.returncode) |
241 | + raise RuntimeError(msg) |
242 | + |
243 | + |
244 | def get_cassandra_yaml_file(): |
245 | return os.path.join(get_cassandra_config_dir(), "cassandra.yaml") |
246 | |
247 | @@ -359,7 +450,10 @@ |
248 | |
249 | def get_cassandra_pid_file(): |
250 | edition = get_cassandra_edition() |
251 | - if edition == 'dse': |
252 | + if edition == 'apache-snap': |
253 | + home = get_snap_env('CASSANDRA_HOME') |
254 | + pid_file = os.path.join(home, 'cassandra.pid') |
255 | + elif edition == 'dse': |
256 | pid_file = "/var/run/dse/dse.pid" |
257 | else: |
258 | pid_file = "/var/run/cassandra/cassandra.pid" |
259 | @@ -367,16 +461,22 @@ |
260 | |
261 | |
262 | def get_cassandra_packages(): |
263 | - edition = get_cassandra_edition() |
264 | - if edition == 'dse': |
265 | - packages = set(['dse-full']) |
266 | - else: |
267 | - packages = set(['cassandra']) # 'cassandra-tools' |
268 | + packages = set() |
269 | |
270 | packages.add('ntp') |
271 | packages.add('run-one') |
272 | packages.add('netcat') |
273 | |
274 | + edition = get_cassandra_edition() |
275 | + if edition == 'apache-snap': |
276 | + packages.add('snapd') |
277 | + return packages |
278 | + |
279 | + if edition == 'dse': |
280 | + packages.add('dse-full') |
281 | + else: |
282 | + packages.add('cassandra') # 'cassandra-tools' |
283 | + |
284 | jre = get_jre() |
285 | if jre == 'oracle': |
286 | # We can't use a packaged version of the Oracle JRE, as we |
287 | @@ -464,6 +564,7 @@ |
288 | # Guard against changing perms on a running db. Although probably |
289 | # harmless, it causes shutil.chown() to fail. |
290 | assert not is_cassandra_running() |
291 | + |
292 | db_dirs = get_all_database_directories() |
293 | ensure_database_directory(db_dirs['commitlog_directory']) |
294 | ensure_database_directory(db_dirs['saved_caches_directory']) |
295 | @@ -549,7 +650,19 @@ |
296 | |
297 | |
298 | def encrypt_password(password): |
299 | - return bcrypt.hashpw(password, bcrypt.gensalt()) |
300 | + password = password.encode('ascii') |
301 | + # Java doesn't understand bcrypt 2b yet: |
302 | + # cassandra.AuthenticationFailed: Failed to authenticate to localhost: |
303 | + # code=0000 [Server error] message="java.lang.IllegalArgumentException: |
304 | + # Invalid salt revision" |
305 | + try: |
306 | + salt = bcrypt.gensalt(prefix=b'2a') |
307 | + # Newer versions of bcrypt return a bytestring. |
308 | + return bcrypt.hashpw(password, salt).decode('ascii') |
309 | + except TypeError: |
310 | + # Trusty bcrypt doesn't support prefix= |
311 | + salt = bcrypt.gensalt() |
312 | + return bcrypt.hashpw(password, salt) |
313 | |
314 | |
315 | @logged |
316 | @@ -563,6 +676,7 @@ |
317 | hookenv.log('Creating SUPERUSER {}'.format(username)) |
318 | else: |
319 | hookenv.log('Creating user {}'.format(username)) |
320 | + |
321 | if has_cassandra_version('2.2'): |
322 | query(session, |
323 | 'INSERT INTO system_auth.roles ' |
324 | @@ -609,7 +723,11 @@ |
325 | |
326 | |
327 | def get_cqlshrc_path(): |
328 | - return os.path.expanduser('~root/.cassandra/cqlshrc') |
329 | + if get_cassandra_edition() == 'apache-snap': |
330 | + base = get_cassandra_config_dir() |
331 | + return os.path.join(base, 'cql/cqlshrc') |
332 | + else: |
333 | + return os.path.expanduser('~root/.cassandra/cqlshrc') |
334 | |
335 | |
336 | def superuser_username(): |
337 | @@ -672,7 +790,11 @@ |
338 | |
339 | |
340 | def nodetool(*cmd, timeout=120): |
341 | - cmd = ['nodetool'] + [str(i) for i in cmd] |
342 | + if get_cassandra_edition() == 'apache-snap': |
343 | + nodetool_cmd = '/snap/bin/cassandra.nodetool' |
344 | + else: |
345 | + nodetool_cmd = 'nodetool' |
346 | + cmd = [nodetool_cmd] + [str(i) for i in cmd] |
347 | i = 0 |
348 | until = time.time() + timeout |
349 | for _ in backoff('nodetool to work'): |
350 | @@ -710,24 +832,39 @@ |
351 | return len(get_bootstrapped_ips()) |
352 | |
353 | |
354 | +def write_config(path, contents): |
355 | + '''Write out the config file at path with the provided contents, encoding |
356 | + in UTF-8 first. If using a snap edition, write to the snap config |
357 | + directory. |
358 | + ''' |
359 | + if get_cassandra_edition() == 'apache-snap': |
360 | + set_snap_config_file(os.path.basename(path), contents) |
361 | + else: |
362 | + contents = contents.encode('UTF-8') |
363 | + host.write_file(path, contents) |
364 | + |
365 | + |
366 | def read_cassandra_yaml(): |
367 | - cassandra_yaml_path = get_cassandra_yaml_file() |
368 | - with open(cassandra_yaml_path, 'rb') as f: |
369 | + if get_cassandra_edition() == 'apache-snap': |
370 | + f = get_snap_config_file('cassandra.yaml') |
371 | return yaml.safe_load(f) |
372 | + else: |
373 | + cassandra_yaml_path = get_cassandra_yaml_file() |
374 | + with open(cassandra_yaml_path, 'rb') as f: |
375 | + return yaml.safe_load(f) |
376 | |
377 | |
378 | @logged |
379 | def write_cassandra_yaml(cassandra_yaml): |
380 | - cassandra_yaml_path = get_cassandra_yaml_file() |
381 | - host.write_file(cassandra_yaml_path, |
382 | - yaml.safe_dump(cassandra_yaml).encode('UTF-8')) |
383 | + write_config(get_cassandra_yaml_file(), yaml.safe_dump(cassandra_yaml)) |
384 | |
385 | |
386 | def configure_cassandra_yaml(overrides={}, seeds=None): |
387 | - cassandra_yaml_path = get_cassandra_yaml_file() |
388 | config = hookenv.config() |
389 | |
390 | - maybe_backup(cassandra_yaml_path) # Its comments may be useful. |
391 | + if get_cassandra_edition() != 'apache-snap': |
392 | + cassandra_yaml_path = get_cassandra_yaml_file() |
393 | + maybe_backup(cassandra_yaml_path) # Its comments may be useful. |
394 | |
395 | cassandra_yaml = read_cassandra_yaml() |
396 | |
397 | @@ -784,6 +921,7 @@ |
398 | |
399 | |
400 | def is_cassandra_running(): |
401 | + edition = get_cassandra_edition() |
402 | pid_file = get_cassandra_pid_file() |
403 | |
404 | try: |
405 | @@ -797,7 +935,12 @@ |
406 | # is not running. |
407 | os.kill(pid, 0) |
408 | |
409 | - if subprocess.call(["nodetool", "status"], |
410 | + if edition == 'apache-snap': |
411 | + # /snap/bin is not on PATH for the root user. |
412 | + nodetool = '/snap/bin/cassandra.nodetool' |
413 | + else: |
414 | + nodetool = 'nodetool' |
415 | + if subprocess.call([nodetool, "status"], |
416 | stdout=subprocess.DEVNULL, |
417 | stderr=subprocess.DEVNULL) == 0: |
418 | hookenv.log( |
419 | |
420 | === modified file 'tests/test_helpers.py' |
421 | --- tests/test_helpers.py 2016-03-17 10:54:10 +0000 |
422 | +++ tests/test_helpers.py 2016-06-29 15:55:10 +0000 |
423 | @@ -163,6 +163,10 @@ |
424 | # Autostart wasn't messed with. |
425 | self.assertFalse(autostart_disabled.called) |
426 | |
427 | + def test_encrypt_password(self): |
428 | + self.assertEqual(type(helpers.encrypt_password('')), str) |
429 | + |
430 | + |
431 | @patch('subprocess.Popen') |
432 | def test_ensure_package_status(self, popen): |
433 | for status in ['install', 'hold']: |
434 | @@ -380,7 +384,10 @@ |
435 | hookenv.config()['edition'] = 'dse' |
436 | self.assertEqual(helpers.get_jre(), 'oracle') |
437 | |
438 | - def test_get_cassandra_edition(self): |
439 | + @patch('charmhelpers.core.host.lsb_release') |
440 | + def test_get_cassandra_edition(self, lsb_release): |
441 | + lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'} |
442 | + |
443 | hookenv.config()['edition'] = 'community' |
444 | self.assertEqual(helpers.get_cassandra_edition(), 'community') |
445 | |
446 | @@ -389,10 +396,21 @@ |
447 | |
448 | self.assertFalse(hookenv.log.called) |
449 | |
450 | + hookenv.log.reset_mock() |
451 | hookenv.config()['edition'] = 'typo' # Default to community |
452 | self.assertEqual(helpers.get_cassandra_edition(), 'community') |
453 | hookenv.log.assert_any_call(ANY, hookenv.ERROR) # Logs an error. |
454 | |
455 | + hookenv.log.reset_mock() |
456 | + hookenv.config()['edition'] = 'apache-snap' # Default to community |
457 | + self.assertEqual(helpers.get_cassandra_edition(), 'community') |
458 | + hookenv.log.assert_any_call(ANY, hookenv.ERROR) # Logs an error. |
459 | + |
460 | + lsb_release.reset_mock() |
461 | + lsb_release.return_value = {'DISTRIB_CODENAME': 'xenial'} |
462 | + hookenv.config()['edition'] = 'apache-snap' |
463 | + self.assertEqual(helpers.get_cassandra_edition(), 'apache-snap') |
464 | + |
465 | @patch('helpers.get_cassandra_edition') |
466 | def test_get_cassandra_service(self, get_edition): |
467 | get_edition.return_value = 'whatever' |
468 | @@ -460,6 +478,30 @@ |
469 | '/foo/cassandra-rackdc.properties') |
470 | |
471 | @patch('helpers.get_cassandra_edition') |
472 | + def test_write_config(self, get_edition): |
473 | + get_edition.return_value = 'whatever' |
474 | + expected = 'some config' |
475 | + with tempfile.TemporaryDirectory() as tmpdir: |
476 | + path = os.path.join(tmpdir, 'foo') |
477 | + helpers.write_config(path, expected) |
478 | + with open(path, 'r') as fp: |
479 | + self.assertEqual(fp.read(), expected) |
480 | + |
481 | + @patch('subprocess.Popen') |
482 | + @patch('helpers.get_cassandra_edition') |
483 | + def test_write_config_snap(self, get_edition, popen): |
484 | + get_edition.return_value = 'apache-snap' |
485 | + popen.return_value.returncode = 0 |
486 | + popen.return_value.communicate.return_value = ('', '') |
487 | + helpers.write_config('/some/path/to/config.yaml', 'some config') |
488 | + expected = 'some config' |
489 | + self.assertEqual( |
490 | + [call(['/snap/bin/cassandra.config-set', 'config.yaml'], |
491 | + stdin=subprocess.PIPE, universal_newlines=True), |
492 | + call().communicate(input=expected)], popen.mock_calls) |
493 | + |
494 | + |
495 | + @patch('helpers.get_cassandra_edition') |
496 | def test_get_cassandra_pid_file(self, get_edition): |
497 | get_edition.return_value = 'whatever' |
498 | self.assertEqual(helpers.get_cassandra_pid_file(), |
Comments inline.