Merge lp:~bcsaller/charms/trusty/cloudfoundry/cmd-refactor into lp:~cf-charmers/charms/trusty/cloudfoundry/trunk
- Trusty Tahr (14.04)
- cmd-refactor
- Merge into trunk
Proposed by
Benjamin Saller
Status: | Merged |
---|---|
Merged at revision: | 165 |
Proposed branch: | lp:~bcsaller/charms/trusty/cloudfoundry/cmd-refactor |
Merge into: | lp:~cf-charmers/charms/trusty/cloudfoundry/trunk |
Diff against target: |
342 lines (+144/-75) 2 files modified
cfdeploy (+24/-9) cloudfoundry/utils.py (+120/-66) |
To merge this branch: | bzr merge lp:~bcsaller/charms/trusty/cloudfoundry/cmd-refactor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Cloud Foundry Charmers | Pending | ||
Review via email: mp+245153@code.launchpad.net |
Commit message
Description of the change
Refactor Command/Commander. Uniform response object, native bool cmp for exit status.
Create a JUJU_REPO in tmp with the current dir linked in for cloudfoundry charm deploy
to work around oddness with links
To post a comment you must log in.
- 164. By Benjamin Saller
-
keyboard interrupt support
- 165. By Benjamin Saller
-
less aggressive reset
- 166. By Benjamin Saller
-
finally fixed
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cfdeploy' | |||
2 | --- cfdeploy 2014-12-03 23:14:23 +0000 | |||
3 | +++ cfdeploy 2014-12-19 18:42:26 +0000 | |||
4 | @@ -29,8 +29,9 @@ | |||
5 | 29 | 29 | ||
6 | 30 | 30 | ||
7 | 31 | def prepare_runtime(): | 31 | def prepare_runtime(): |
8 | 32 | in_venv = os.environ.get('VIRTUAL_ENV', None) | ||
9 | 32 | activate = "bin/activate_this.py" | 33 | activate = "bin/activate_this.py" |
11 | 33 | if not verify_dep('virtualenv'): | 34 | if not verify_dep('virtualenv') and not in_venv: |
12 | 34 | print "(sudo) installing python-virtualenv package." | 35 | print "(sudo) installing python-virtualenv package." |
13 | 35 | try: | 36 | try: |
14 | 36 | subprocess.check_output( | 37 | subprocess.check_output( |
15 | @@ -39,12 +40,13 @@ | |||
16 | 39 | except subprocess.CalledProcessError as e: | 40 | except subprocess.CalledProcessError as e: |
17 | 40 | print e.output | 41 | print e.output |
18 | 41 | raise | 42 | raise |
25 | 42 | import virtualenv | 43 | if not in_venv: |
26 | 43 | venv = os.path.join(os.getcwd(), '.venv') | 44 | import virtualenv |
27 | 44 | if not os.path.exists(venv): | 45 | venv = os.path.join(os.getcwd(), '.venv') |
28 | 45 | virtualenv.create_environment(venv, site_packages=False) | 46 | if not os.path.exists(venv): |
29 | 46 | venv_launcher = os.path.join(venv, activate) | 47 | virtualenv.create_environment(venv, site_packages=False) |
30 | 47 | execfile(venv_launcher, dict(__file__=venv_launcher)) | 48 | venv_launcher = os.path.join(venv, activate) |
31 | 49 | execfile(venv_launcher, dict(__file__=venv_launcher)) | ||
32 | 48 | install_python_deps() | 50 | install_python_deps() |
33 | 49 | 51 | ||
34 | 50 | prepare_runtime() | 52 | prepare_runtime() |
35 | @@ -69,6 +71,11 @@ | |||
36 | 69 | 71 | ||
37 | 70 | from progress.bar import Bar | 72 | from progress.bar import Bar |
38 | 71 | 73 | ||
39 | 74 | def reset_terminal(): | ||
40 | 75 | subprocess.check_call(['stty', 'sane']) | ||
41 | 76 | subprocess.check_call(['tput', 'rs2', 'rs3']) | ||
42 | 77 | |||
43 | 78 | |||
44 | 72 | 79 | ||
45 | 73 | def setup(): | 80 | def setup(): |
46 | 74 | parser = argparse.ArgumentParser() | 81 | parser = argparse.ArgumentParser() |
47 | @@ -100,6 +107,13 @@ | |||
48 | 100 | yield | 107 | yield |
49 | 101 | sys.stderr = _save | 108 | sys.stderr = _save |
50 | 102 | 109 | ||
51 | 110 | def reset_handler(f): | ||
52 | 111 | def wrapper(*args, **kwargs): | ||
53 | 112 | try: | ||
54 | 113 | f(*args, **kwargs) | ||
55 | 114 | finally: | ||
56 | 115 | reset_terminal() | ||
57 | 116 | return wrapper | ||
58 | 103 | 117 | ||
59 | 104 | def show_reconciler(): | 118 | def show_reconciler(): |
60 | 105 | with devnull(): | 119 | with devnull(): |
61 | @@ -136,13 +150,14 @@ | |||
62 | 136 | print "Installing CF CLI (this requires sudo access)" | 150 | print "Installing CF CLI (this requires sudo access)" |
63 | 137 | sh.sudo('dpkg', '-i', 'cf-cli_amd64.deb') | 151 | sh.sudo('dpkg', '-i', 'cf-cli_amd64.deb') |
64 | 138 | 152 | ||
66 | 139 | 153 | @reset_handler | |
67 | 140 | def main(): | 154 | def main(): |
68 | 141 | options = setup() | 155 | options = setup() |
69 | 142 | if options.log: | 156 | if options.log: |
70 | 143 | root_logger = logging.getLogger() | 157 | root_logger = logging.getLogger() |
71 | 144 | root_logger.handlers = [logging.FileHandler('cfdeploy.log', 'w')] | 158 | root_logger.handlers = [logging.FileHandler('cfdeploy.log', 'w')] |
72 | 145 | root_logger.setLevel(logging.DEBUG) | 159 | root_logger.setLevel(logging.DEBUG) |
73 | 160 | sh.set_log(root_logger) | ||
74 | 146 | install_deps() | 161 | install_deps() |
75 | 147 | 162 | ||
76 | 148 | bar = ProgressBar('Deploying CloudFoundry', max=10) | 163 | bar = ProgressBar('Deploying CloudFoundry', max=10) |
77 | @@ -169,7 +184,7 @@ | |||
78 | 169 | bar.next(message="Opening Admin Console") | 184 | bar.next(message="Opening Admin Console") |
79 | 170 | show_webadmin() | 185 | show_webadmin() |
80 | 171 | bar.finish() | 186 | bar.finish() |
82 | 172 | 187 | subprocess.check_call(['tput', 'rs2', 'rs3']) | |
83 | 173 | print "You should now be logged into a running CF deployment." | 188 | print "You should now be logged into a running CF deployment." |
84 | 174 | if which('cf'): | 189 | if which('cf'): |
85 | 175 | print "The 'cf' command line client is installed and available." | 190 | print "The 'cf' command line client is installed and available." |
86 | 176 | 191 | ||
87 | === modified file 'cloudfoundry/utils.py' | |||
88 | --- cloudfoundry/utils.py 2014-12-09 22:56:46 +0000 | |||
89 | +++ cloudfoundry/utils.py 2014-12-19 18:42:26 +0000 | |||
90 | @@ -4,9 +4,9 @@ | |||
91 | 4 | import logging | 4 | import logging |
92 | 5 | import os | 5 | import os |
93 | 6 | import re | 6 | import re |
94 | 7 | import shutil | ||
95 | 7 | import socket | 8 | import socket |
96 | 8 | import subprocess | 9 | import subprocess |
97 | 9 | import sys | ||
98 | 10 | import tempfile | 10 | import tempfile |
99 | 11 | import time | 11 | import time |
100 | 12 | from contextlib import contextmanager | 12 | from contextlib import contextmanager |
101 | @@ -17,7 +17,7 @@ | |||
102 | 17 | 17 | ||
103 | 18 | from charmhelpers import fetch | 18 | from charmhelpers import fetch |
104 | 19 | 19 | ||
106 | 20 | 20 | log = logging.getLogger('utils') | |
107 | 21 | ip_pat = re.compile('^(\d{1,3}\.){3}\d{1,3}$') | 21 | ip_pat = re.compile('^(\d{1,3}\.){3}\d{1,3}$') |
108 | 22 | _client = None # cached client connection | 22 | _client = None # cached client connection |
109 | 23 | 23 | ||
110 | @@ -168,52 +168,115 @@ | |||
111 | 168 | return conf | 168 | return conf |
112 | 169 | 169 | ||
113 | 170 | 170 | ||
122 | 171 | def command(*base_args, **kwargs): | 171 | class ProcessResult(object): |
123 | 172 | check = kwargs.pop('check', False) | 172 | def __init__(self, command, exit_code, stdout, stderr): |
124 | 173 | throw = kwargs.pop('throw', True) | 173 | self.command = command |
125 | 174 | 174 | self.exit_code = exit_code | |
126 | 175 | def callable_command(*args, **kws): | 175 | self.stdout = stdout |
127 | 176 | kwargs.update(kws) | 176 | self.stderr = stderr |
128 | 177 | all_args = base_args + args | 177 | |
129 | 178 | logging.debug(' '.join(all_args)) | 178 | def __repr__(self): |
130 | 179 | return '<ProcessResult "%s":%s>' % (self.command, self.exit_code) | ||
131 | 180 | |||
132 | 181 | @property | ||
133 | 182 | def cmd(self): | ||
134 | 183 | return ' '.join(self.command) | ||
135 | 184 | |||
136 | 185 | @property | ||
137 | 186 | def output(self): | ||
138 | 187 | result = '' | ||
139 | 188 | if self.stdout: | ||
140 | 189 | result += self.stdout | ||
141 | 190 | if self.stderr: | ||
142 | 191 | result += self.stderr | ||
143 | 192 | return result.strip() | ||
144 | 193 | |||
145 | 194 | @property | ||
146 | 195 | def json(self): | ||
147 | 196 | if self.stdout: | ||
148 | 197 | return json.loads(self.stdout) | ||
149 | 198 | return None | ||
150 | 199 | |||
151 | 200 | def __eq__(self, other): | ||
152 | 201 | return self.exit_code == other | ||
153 | 202 | |||
154 | 203 | def __bool__(self): | ||
155 | 204 | return self.exit_code == 0 | ||
156 | 205 | |||
157 | 206 | def throw_on_error(self): | ||
158 | 207 | if not bool(self): | ||
159 | 208 | raise subprocess.CalledProcessError( | ||
160 | 209 | self.exit_code, self.command, output=self.output) | ||
161 | 210 | |||
162 | 211 | |||
163 | 212 | class Process(object): | ||
164 | 213 | def __init__(self, command=None, throw=False, log=log, **kwargs): | ||
165 | 214 | if isinstance(command, str): | ||
166 | 215 | command = (command, ) | ||
167 | 216 | self.command = command | ||
168 | 217 | self._throw_on_error = False | ||
169 | 218 | self.log = log | ||
170 | 219 | self._kw = kwargs | ||
171 | 220 | |||
172 | 221 | def throw_on_error(self, throw=True): | ||
173 | 222 | self._throw_on_error = throw | ||
174 | 223 | return self | ||
175 | 224 | |||
176 | 225 | def __call__(self, *args, **kw): | ||
177 | 226 | kwargs = dict(stdout=subprocess.PIPE, | ||
178 | 227 | stderr=subprocess.STDOUT) | ||
179 | 228 | if self._kw: | ||
180 | 229 | kwargs.update(self._kw) | ||
181 | 230 | kwargs.update(kw) | ||
182 | 231 | if self.command: | ||
183 | 232 | all_args = self.command + args | ||
184 | 233 | else: | ||
185 | 234 | all_args = args | ||
186 | 179 | if 'env' not in kwargs: | 235 | if 'env' not in kwargs: |
187 | 180 | kwargs['env'] = os.environ | 236 | kwargs['env'] = os.environ |
189 | 181 | logging.debug("invoke: %s", all_args) | 237 | p = subprocess.Popen(all_args, **kwargs) |
190 | 238 | stdout, stderr = p.communicate() | ||
191 | 239 | self.log.debug(stdout) | ||
192 | 240 | stdout = stdout.strip() | ||
193 | 241 | if stderr is not None: | ||
194 | 242 | stderr = stderr.strip() | ||
195 | 243 | self.log.debug(stderr) | ||
196 | 244 | exit_code = p.poll() | ||
197 | 245 | result = ProcessResult(all_args, exit_code, stdout, stderr) | ||
198 | 246 | self.log.debug("process: %s (%d)", result.cmd, result.exit_code) | ||
199 | 247 | if self._throw_on_error and not bool(self): | ||
200 | 248 | result.throw_on_error() | ||
201 | 249 | return result | ||
202 | 182 | 250 | ||
217 | 183 | p = subprocess.Popen(all_args, | 251 | command = Process |
204 | 184 | stdout=subprocess.PIPE, | ||
205 | 185 | stderr=subprocess.STDOUT, | ||
206 | 186 | **kwargs) | ||
207 | 187 | output, _ = p.communicate() | ||
208 | 188 | ret_code = p.poll() | ||
209 | 189 | logging.debug('output: %s', output) | ||
210 | 190 | if check is True: | ||
211 | 191 | if ret_code != 0 and throw: | ||
212 | 192 | raise subprocess.CalledProcessError(ret_code, all_args, output=output) | ||
213 | 193 | return ret_code | ||
214 | 194 | else: | ||
215 | 195 | return output.strip() | ||
216 | 196 | return callable_command | ||
218 | 197 | 252 | ||
219 | 198 | 253 | ||
220 | 199 | class Commander(object): | 254 | class Commander(object): |
221 | 255 | def __init__(self, log=log): | ||
222 | 256 | self.log = log | ||
223 | 257 | |||
224 | 258 | def set_log(self, logger): | ||
225 | 259 | self.log = logger | ||
226 | 260 | |||
227 | 200 | def __getattr__(self, key): | 261 | def __getattr__(self, key): |
229 | 201 | return command(key) | 262 | return command((key,), log=self.log) |
230 | 202 | 263 | ||
231 | 203 | def check(self, *args, **kwargs): | 264 | def check(self, *args, **kwargs): |
233 | 204 | return command(*args, check=True, **kwargs) | 265 | kwargs.update({'log': self.log}) |
234 | 266 | return command(args, **kwargs).throw_on_error() | ||
235 | 205 | 267 | ||
238 | 206 | def __call__(self, cmd, **kwargs): | 268 | def __call__(self, *args, **kwargs): |
239 | 207 | subprocess.check_output(cmd, shell=True, **kwargs) | 269 | kwargs.update({'log': self.log}) |
240 | 270 | return command(args, shell=True, **kwargs) | ||
241 | 208 | 271 | ||
242 | 209 | 272 | ||
243 | 210 | sh = Commander() | 273 | sh = Commander() |
244 | 211 | dig = command('dig', '+short') | 274 | dig = command('dig', '+short') |
246 | 212 | api_endpoints = sh.check('juju', 'api-endpoints', throw=False) | 275 | api_endpoints = sh('juju', 'api-endpoints') |
247 | 213 | 276 | ||
248 | 214 | 277 | ||
249 | 215 | def current_env(): | 278 | def current_env(): |
251 | 216 | return sh.juju('switch') | 279 | return sh.juju('switch').output |
252 | 217 | 280 | ||
253 | 218 | 281 | ||
254 | 219 | def get_jenv(): | 282 | def get_jenv(): |
255 | @@ -285,9 +348,9 @@ | |||
256 | 285 | 348 | ||
257 | 286 | 349 | ||
258 | 287 | def juju_state_server(): | 350 | def juju_state_server(): |
260 | 288 | if api_endpoints() != 0: | 351 | if not api_endpoints(): |
261 | 289 | return False | 352 | return False |
263 | 290 | endpoints = json.loads(sh.juju('api-endpoints', '--format=json')) | 353 | endpoints = sh.juju('api-endpoints', '--format=json').json |
264 | 291 | for ep in endpoints: | 354 | for ep in endpoints: |
265 | 292 | host, port = ep.split(':', 1) | 355 | host, port = ep.split(':', 1) |
266 | 293 | result = socket_open(host, int(port)) | 356 | result = socket_open(host, int(port)) |
267 | @@ -374,9 +437,8 @@ | |||
268 | 374 | 437 | ||
269 | 375 | 438 | ||
270 | 376 | def bootstrap(): | 439 | def bootstrap(): |
274 | 377 | if not os.path.exists(get_jenv()) or api_endpoints() != 0: | 440 | if not os.path.exists(get_jenv()) or not api_endpoints(): |
275 | 378 | juju = sh.check('juju', throw=False) | 441 | return bool(sh.juju('bootstrap', '--upload-tools')) |
273 | 379 | return juju('bootstrap') != 0 | ||
276 | 380 | return True | 442 | return True |
277 | 381 | 443 | ||
278 | 382 | 444 | ||
279 | @@ -386,22 +448,16 @@ | |||
280 | 386 | return data['password'] | 448 | return data['password'] |
281 | 387 | 449 | ||
282 | 388 | 450 | ||
299 | 389 | def get_repo_path(): | 451 | @contextmanager |
300 | 390 | candidates = [os.getcwd(), os.environ.get('JUJU_REPOSITORY')] | 452 | def cf_repo(): |
301 | 391 | for repo_path in candidates: | 453 | """Context manager to ensure we are deploying the cloudfoundry charm |
302 | 392 | if not repo_path: | 454 | containing this instance of cfdeploy.""" |
303 | 393 | continue | 455 | repo = tempfile.mkdtemp() |
304 | 394 | if 'trusty' in repo_path: | 456 | repo_path = os.path.join(repo, "trusty") |
305 | 395 | repo_path, _ = os.path.split(repo_path) | 457 | os.mkdir(repo_path) |
306 | 396 | if repo_path.endswith('trusty'): | 458 | os.symlink(os.path.abspath(os.getcwd()), os.path.join(repo_path, "cloudfoundry")) |
307 | 397 | repo_path = os.path.dirname(repo_path) | 459 | yield repo |
308 | 398 | if os.path.exists(os.path.join(repo_path, 'trusty', 'cloudfoundry')): | 460 | shutil.rmtree(repo) |
293 | 399 | return repo_path | ||
294 | 400 | sys.stderr.write( | ||
295 | 401 | 'Unable to determine valid repository path.\n' | ||
296 | 402 | 'Please set JUJU_REPOSITORY and/or ensure that directory containing\n' | ||
297 | 403 | 'the charm includes the series.\n') | ||
298 | 404 | sys.exit(1) | ||
309 | 405 | 461 | ||
310 | 406 | 462 | ||
311 | 407 | def deploy(**config): | 463 | def deploy(**config): |
312 | @@ -421,20 +477,18 @@ | |||
313 | 421 | with open(fn, 'w') as fp: | 477 | with open(fn, 'w') as fp: |
314 | 422 | yaml.dump({'cloudfoundry': config}, fp) | 478 | yaml.dump({'cloudfoundry': config}, fp) |
315 | 423 | 479 | ||
330 | 424 | repo_path = get_repo_path() | 480 | with cf_repo() as repo_path: |
331 | 425 | 481 | args = ['deploy', '--config=%s' % fn, | |
332 | 426 | args = ['deploy', '--config=%s' % fn, | 482 | '--repository=%s' % repo_path] |
333 | 427 | '--repository=%s' % repo_path] | 483 | if constraints: |
334 | 428 | if constraints: | 484 | args.append('--constraints=%s' % constraints) |
335 | 429 | args.append('--constraints=%s' % constraints) | 485 | args.append('local:trusty/cloudfoundry') |
336 | 430 | args.append('local:trusty/cloudfoundry') | 486 | if not sh.juju(*args): |
337 | 431 | juju = sh.check('juju', throw=False) | 487 | return False |
338 | 432 | if juju(*args) != 0: | 488 | time.sleep(5) |
339 | 433 | return False | 489 | if not sh.juju('expose', 'cloudfoundry'): |
340 | 434 | time.sleep(5) | 490 | return False |
341 | 435 | if juju('expose', 'cloudfoundry') != 0: | 491 | os.unlink(fn) |
328 | 436 | return False | ||
329 | 437 | os.unlink(fn) | ||
342 | 438 | return True | 492 | return True |
343 | 439 | 493 | ||
344 | 440 | 494 |