Merge lp:~bcsaller/charms/trusty/cloudfoundry/cmd-refactor into lp:~cf-charmers/charms/trusty/cloudfoundry/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
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+245153@code.launchpad.net

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
6
7 def prepare_runtime():
8+ in_venv = os.environ.get('VIRTUAL_ENV', None)
9 activate = "bin/activate_this.py"
10- if not verify_dep('virtualenv'):
11+ if not verify_dep('virtualenv') and not in_venv:
12 print "(sudo) installing python-virtualenv package."
13 try:
14 subprocess.check_output(
15@@ -39,12 +40,13 @@
16 except subprocess.CalledProcessError as e:
17 print e.output
18 raise
19- import virtualenv
20- venv = os.path.join(os.getcwd(), '.venv')
21- if not os.path.exists(venv):
22- virtualenv.create_environment(venv, site_packages=False)
23- venv_launcher = os.path.join(venv, activate)
24- execfile(venv_launcher, dict(__file__=venv_launcher))
25+ if not in_venv:
26+ import virtualenv
27+ venv = os.path.join(os.getcwd(), '.venv')
28+ if not os.path.exists(venv):
29+ virtualenv.create_environment(venv, site_packages=False)
30+ venv_launcher = os.path.join(venv, activate)
31+ execfile(venv_launcher, dict(__file__=venv_launcher))
32 install_python_deps()
33
34 prepare_runtime()
35@@ -69,6 +71,11 @@
36
37 from progress.bar import Bar
38
39+def reset_terminal():
40+ subprocess.check_call(['stty', 'sane'])
41+ subprocess.check_call(['tput', 'rs2', 'rs3'])
42+ print
43+
44
45 def setup():
46 parser = argparse.ArgumentParser()
47@@ -100,6 +107,13 @@
48 yield
49 sys.stderr = _save
50
51+def reset_handler(f):
52+ def wrapper(*args, **kwargs):
53+ try:
54+ f(*args, **kwargs)
55+ finally:
56+ reset_terminal()
57+ return wrapper
58
59 def show_reconciler():
60 with devnull():
61@@ -136,13 +150,14 @@
62 print "Installing CF CLI (this requires sudo access)"
63 sh.sudo('dpkg', '-i', 'cf-cli_amd64.deb')
64
65-
66+@reset_handler
67 def main():
68 options = setup()
69 if options.log:
70 root_logger = logging.getLogger()
71 root_logger.handlers = [logging.FileHandler('cfdeploy.log', 'w')]
72 root_logger.setLevel(logging.DEBUG)
73+ sh.set_log(root_logger)
74 install_deps()
75
76 bar = ProgressBar('Deploying CloudFoundry', max=10)
77@@ -169,7 +184,7 @@
78 bar.next(message="Opening Admin Console")
79 show_webadmin()
80 bar.finish()
81-
82+ subprocess.check_call(['tput', 'rs2', 'rs3'])
83 print "You should now be logged into a running CF deployment."
84 if which('cf'):
85 print "The 'cf' command line client is installed and available."
86
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 import logging
92 import os
93 import re
94+import shutil
95 import socket
96 import subprocess
97-import sys
98 import tempfile
99 import time
100 from contextlib import contextmanager
101@@ -17,7 +17,7 @@
102
103 from charmhelpers import fetch
104
105-
106+log = logging.getLogger('utils')
107 ip_pat = re.compile('^(\d{1,3}\.){3}\d{1,3}$')
108 _client = None # cached client connection
109
110@@ -168,52 +168,115 @@
111 return conf
112
113
114-def command(*base_args, **kwargs):
115- check = kwargs.pop('check', False)
116- throw = kwargs.pop('throw', True)
117-
118- def callable_command(*args, **kws):
119- kwargs.update(kws)
120- all_args = base_args + args
121- logging.debug(' '.join(all_args))
122+class ProcessResult(object):
123+ def __init__(self, command, exit_code, stdout, stderr):
124+ self.command = command
125+ self.exit_code = exit_code
126+ self.stdout = stdout
127+ self.stderr = stderr
128+
129+ def __repr__(self):
130+ return '<ProcessResult "%s":%s>' % (self.command, self.exit_code)
131+
132+ @property
133+ def cmd(self):
134+ return ' '.join(self.command)
135+
136+ @property
137+ def output(self):
138+ result = ''
139+ if self.stdout:
140+ result += self.stdout
141+ if self.stderr:
142+ result += self.stderr
143+ return result.strip()
144+
145+ @property
146+ def json(self):
147+ if self.stdout:
148+ return json.loads(self.stdout)
149+ return None
150+
151+ def __eq__(self, other):
152+ return self.exit_code == other
153+
154+ def __bool__(self):
155+ return self.exit_code == 0
156+
157+ def throw_on_error(self):
158+ if not bool(self):
159+ raise subprocess.CalledProcessError(
160+ self.exit_code, self.command, output=self.output)
161+
162+
163+class Process(object):
164+ def __init__(self, command=None, throw=False, log=log, **kwargs):
165+ if isinstance(command, str):
166+ command = (command, )
167+ self.command = command
168+ self._throw_on_error = False
169+ self.log = log
170+ self._kw = kwargs
171+
172+ def throw_on_error(self, throw=True):
173+ self._throw_on_error = throw
174+ return self
175+
176+ def __call__(self, *args, **kw):
177+ kwargs = dict(stdout=subprocess.PIPE,
178+ stderr=subprocess.STDOUT)
179+ if self._kw:
180+ kwargs.update(self._kw)
181+ kwargs.update(kw)
182+ if self.command:
183+ all_args = self.command + args
184+ else:
185+ all_args = args
186 if 'env' not in kwargs:
187 kwargs['env'] = os.environ
188- logging.debug("invoke: %s", all_args)
189+ p = subprocess.Popen(all_args, **kwargs)
190+ stdout, stderr = p.communicate()
191+ self.log.debug(stdout)
192+ stdout = stdout.strip()
193+ if stderr is not None:
194+ stderr = stderr.strip()
195+ self.log.debug(stderr)
196+ exit_code = p.poll()
197+ result = ProcessResult(all_args, exit_code, stdout, stderr)
198+ self.log.debug("process: %s (%d)", result.cmd, result.exit_code)
199+ if self._throw_on_error and not bool(self):
200+ result.throw_on_error()
201+ return result
202
203- p = subprocess.Popen(all_args,
204- stdout=subprocess.PIPE,
205- stderr=subprocess.STDOUT,
206- **kwargs)
207- output, _ = p.communicate()
208- ret_code = p.poll()
209- logging.debug('output: %s', output)
210- if check is True:
211- if ret_code != 0 and throw:
212- raise subprocess.CalledProcessError(ret_code, all_args, output=output)
213- return ret_code
214- else:
215- return output.strip()
216- return callable_command
217+command = Process
218
219
220 class Commander(object):
221+ def __init__(self, log=log):
222+ self.log = log
223+
224+ def set_log(self, logger):
225+ self.log = logger
226+
227 def __getattr__(self, key):
228- return command(key)
229+ return command((key,), log=self.log)
230
231 def check(self, *args, **kwargs):
232- return command(*args, check=True, **kwargs)
233+ kwargs.update({'log': self.log})
234+ return command(args, **kwargs).throw_on_error()
235
236- def __call__(self, cmd, **kwargs):
237- subprocess.check_output(cmd, shell=True, **kwargs)
238+ def __call__(self, *args, **kwargs):
239+ kwargs.update({'log': self.log})
240+ return command(args, shell=True, **kwargs)
241
242
243 sh = Commander()
244 dig = command('dig', '+short')
245-api_endpoints = sh.check('juju', 'api-endpoints', throw=False)
246+api_endpoints = sh('juju', 'api-endpoints')
247
248
249 def current_env():
250- return sh.juju('switch')
251+ return sh.juju('switch').output
252
253
254 def get_jenv():
255@@ -285,9 +348,9 @@
256
257
258 def juju_state_server():
259- if api_endpoints() != 0:
260+ if not api_endpoints():
261 return False
262- endpoints = json.loads(sh.juju('api-endpoints', '--format=json'))
263+ endpoints = sh.juju('api-endpoints', '--format=json').json
264 for ep in endpoints:
265 host, port = ep.split(':', 1)
266 result = socket_open(host, int(port))
267@@ -374,9 +437,8 @@
268
269
270 def bootstrap():
271- if not os.path.exists(get_jenv()) or api_endpoints() != 0:
272- juju = sh.check('juju', throw=False)
273- return juju('bootstrap') != 0
274+ if not os.path.exists(get_jenv()) or not api_endpoints():
275+ return bool(sh.juju('bootstrap', '--upload-tools'))
276 return True
277
278
279@@ -386,22 +448,16 @@
280 return data['password']
281
282
283-def get_repo_path():
284- candidates = [os.getcwd(), os.environ.get('JUJU_REPOSITORY')]
285- for repo_path in candidates:
286- if not repo_path:
287- continue
288- if 'trusty' in repo_path:
289- repo_path, _ = os.path.split(repo_path)
290- if repo_path.endswith('trusty'):
291- repo_path = os.path.dirname(repo_path)
292- if os.path.exists(os.path.join(repo_path, 'trusty', 'cloudfoundry')):
293- return repo_path
294- sys.stderr.write(
295- 'Unable to determine valid repository path.\n'
296- 'Please set JUJU_REPOSITORY and/or ensure that directory containing\n'
297- 'the charm includes the series.\n')
298- sys.exit(1)
299+@contextmanager
300+def cf_repo():
301+ """Context manager to ensure we are deploying the cloudfoundry charm
302+ containing this instance of cfdeploy."""
303+ repo = tempfile.mkdtemp()
304+ repo_path = os.path.join(repo, "trusty")
305+ os.mkdir(repo_path)
306+ os.symlink(os.path.abspath(os.getcwd()), os.path.join(repo_path, "cloudfoundry"))
307+ yield repo
308+ shutil.rmtree(repo)
309
310
311 def deploy(**config):
312@@ -421,20 +477,18 @@
313 with open(fn, 'w') as fp:
314 yaml.dump({'cloudfoundry': config}, fp)
315
316- repo_path = get_repo_path()
317-
318- args = ['deploy', '--config=%s' % fn,
319- '--repository=%s' % repo_path]
320- if constraints:
321- args.append('--constraints=%s' % constraints)
322- args.append('local:trusty/cloudfoundry')
323- juju = sh.check('juju', throw=False)
324- if juju(*args) != 0:
325- return False
326- time.sleep(5)
327- if juju('expose', 'cloudfoundry') != 0:
328- return False
329- os.unlink(fn)
330+ with cf_repo() as repo_path:
331+ args = ['deploy', '--config=%s' % fn,
332+ '--repository=%s' % repo_path]
333+ if constraints:
334+ args.append('--constraints=%s' % constraints)
335+ args.append('local:trusty/cloudfoundry')
336+ if not sh.juju(*args):
337+ return False
338+ time.sleep(5)
339+ if not sh.juju('expose', 'cloudfoundry'):
340+ return False
341+ os.unlink(fn)
342 return True
343
344

Subscribers

People subscribed via source and target branches