Merge lp:~raharper/curtin/trunk.curtin-announce into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Merged
Approved by: Scott Moser
Approved revision: 434
Merged at revision: 448
Proposed branch: lp:~raharper/curtin/trunk.curtin-announce
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 280 lines (+179/-1)
8 files modified
curtin/__init__.py (+2/-0)
curtin/commands/main.py (+6/-1)
curtin/pack.py (+1/-0)
curtin/version.py (+52/-0)
debian/rules (+6/-0)
tests/unittests/test_version.py (+87/-0)
tests/vmtests/__init__.py (+18/-0)
tests/vmtests/test_basic.py (+7/-0)
To merge this branch: bzr merge lp:~raharper/curtin/trunk.curtin-announce
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
curtin developers Pending
Review via email: mp+306358@code.launchpad.net

Commit message

curtin: add version module and display in output and logs

It's difficult to determine what curtin version is running
from the collected logs in a maas deployment which uses
curtin by sending it to the target system. Resolve this by
having curtin announce itself.

- Introduce a new version module which does the following:
  - search for a .version file in the curtin PYTHONPATH
    if present and use that file contents for version
  - if no .version file is found, test if we're in-tree
    and use the existing curtin.__version__ value plus the
    bzr revno
  - if neither are found, just use the curtin.__version__ value
- Modify curtin.command.main to print the version string in argparse
  output
- Update curtin logging setup to emit a version string as soon as logging
  is configured

Description of the change

curtin: add version module and display in output and logs

It's difficult to determine what curtin version is running
from the collected logs in a maas deployment which uses
curtin by sending it to the target system. Resolve this by
having curtin announce itself.

- Introduce a new version module which does the following:
  - search for a .version file in the curtin PYTHONPATH
    if present and use that file contents for version
  - if no .version file is found, test if we're in-tree
    and use the existing curtin.__version__ value plus the
    bzr revno
  - if neither are found, just use the curtin.__version__ value
- Modify curtin.command.main to print the version string in argparse
  output
- Update curtin logging setup to emit a version string as soon as logging
  is configured

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

It seems like a generally nice idea. a couple of things though

I think it'd be good to get the dpkg info if it looks like it is installed.
possibly if the __file__ here is in some set of expected install paths... the problem with that is that dpkg -S 'filename' is heavy, and requires dpkg.

Alternatively, and maybe better, we could have the package install a file that states its version.
and then just read that.

Revision history for this message
Ryan Harper (raharper) wrote :

On Thu, Jan 19, 2017 at 12:30 PM, Scott Moser <email address hidden> wrote:

> It seems like a generally nice idea. a couple of things though
>
> I think it'd be good to get the dpkg info if it looks like it is installed.
> possibly if the __file__ here is in some set of expected install paths...
> the problem with that is that dpkg -S 'filename' is heavy, and requires
> dpkg.
>
> Alternatively, and maybe better, we could have the package install a file
> that states its version.
> and then just read that.
>
> Diff comments:
>
> >
> > === added file 'curtin/version.py'
> > --- curtin/version.py 1970-01-01 00:00:00 +0000
> > +++ curtin/version.py 2016-09-21 16:08:03 +0000
> > @@ -0,0 +1,40 @@
> > +from curtin import __version__ as old_version
> > +import os
> > +import subprocess
> > +
> > +
> > +def version_string():
> > + """ Extract a version string from curtin source or version file"""
> > + def _find_path(pathfile):
> > + """ Check for file existance and return dirpath
> > + Search PYTHONPATH, as curtin is typically
> > + launched with PYTHONPATH set, as with curtin pack
> > + executables.
> > + """
> > + if os.path.exists(pathfile):
> > + return os.getcwd()
> > + elif 'PYTHONPATH' in os.environ:
> > + for path in os.environ['PYTHONPATH'].split(":"):
> > + curpath = os.path.join(path, pathfile)
> > + if os.path.exists(curpath):
> > + return os.path.dirname(curpath)
> > +
> > + return None
> > +
>
> any reason not to use __file__ ? rather than PYTHONPATH ? python path just
> seems like it might go wrong.
>

I didn't know of __file__; fixing.

>
> > + dotversion = '.version'
> > + dotpath = _find_path(dotversion)
> > + if dotpath:
> > + return open(os.path.join(dotpath, dotversion),
> 'r').read().strip()
> > +
> > + bzrdir = _find_path('.bzr')
> > + revno = None
> > + if bzrdir:
> > + os.chdir(bzrdir)
> > + out = subprocess.check_output(['bzr', 'revno'])
> > + revno = "bzr%s" % out.decode('ascii').strip()
>
> any reason for ascii ?
>

Not sure why. I can 'utf-8' instead.

> Again here, i think better to just use the path that we know. .bzr will
> be 2 dirs up from this file name .
>

If I use __file__ above, then we get that.

>
> and we need to catch an exception here if there is a bzr dir but no bzr
> command.
>

Are there really .bzr dirs in a orig tarball? Otherwise, I don't see how
we'd have curtin source without bzr?

>
> > +
> > + version = old_version
> > + if revno:
> > + version += "~%s" % revno
> > +
> > + return version
>
>
> --
> https://code.launchpad.net/~raharper/curtin/trunk.curtin-
> announce/+merge/306358
> You are the owner of lp:~raharper/curtin/trunk.curtin-announce.
>

428. By Ryan Harper

Use __file__, catch subprocess exception; export LANG into install log

429. By Ryan Harper

merge from trunk

430. By Ryan Harper

merge from lp:~smoser/curtin/trunk.curtin-announce

431. By Ryan Harper

pep fix for merge

432. By Ryan Harper

version: drop .version file; add dpkg-version, unittests

433. By Ryan Harper

merge from trunk

434. By Ryan Harper

merge from trunk

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'curtin/__init__.py'
2--- curtin/__init__.py 2016-08-18 16:02:27 +0000
3+++ curtin/__init__.py 2017-02-06 22:02:13 +0000
4@@ -35,6 +35,8 @@
5 'SUBCOMMAND_SYSTEM_UPGRADE',
6 # supports new format of apt configuration
7 'APT_CONFIG_V1',
8+ # has version module
9+ 'HAS_VERSION_MODULE',
10 ]
11
12 __version__ = "0.1.0"
13
14=== modified file 'curtin/commands/main.py'
15--- curtin/commands/main.py 2016-08-10 21:44:19 +0000
16+++ curtin/commands/main.py 2017-02-06 22:02:13 +0000
17@@ -24,6 +24,9 @@
18 from .. import log
19 from .. import util
20 from ..deps import install_deps
21+import curtin.version
22+
23+VERSIONSTR = curtin.version.version_string()
24
25 SUB_COMMAND_MODULES = [
26 'apply_net', 'block-info', 'block-meta', 'block-wipe', 'curthooks',
27@@ -57,7 +60,7 @@
28
29 def get_main_parser(stacktrace=False, verbosity=0,
30 parser_class=argparse.ArgumentParser):
31- parser = parser_class(prog='curtin')
32+ parser = parser_class(prog='curtin', epilog='Version %s' % VERSIONSTR)
33 parser.add_argument('--showtrace', action='store_true', default=stacktrace)
34 parser.add_argument('-v', '--verbose', action='count', default=verbosity,
35 dest='verbosity')
36@@ -180,6 +183,8 @@
37 sys.exit(1)
38
39 log.basicConfig(stream=args.log_file, verbosity=verbosity)
40+ log.LOG.error('curtin v. %s started' % VERSIONSTR)
41+ log.LOG.error('LANG=%s', os.environ.get('LANG'))
42
43 paths = util.get_paths()
44
45
46=== modified file 'curtin/pack.py'
47--- curtin/pack.py 2015-12-17 16:19:56 +0000
48+++ curtin/pack.py 2017-02-06 22:02:13 +0000
49@@ -140,6 +140,7 @@
50 shutil.copytree(paths['helpers'], os.path.join(exdir, "helpers"))
51 shutil.copytree(paths['lib'], os.path.join(exdir, "curtin"),
52 ignore=not_dot_py)
53+
54 write_exe_wrapper(entrypoint='curtin.commands.main',
55 path=os.path.join(bindir, 'curtin'),
56 deps_check_entry="curtin.deps.check")
57
58=== added file 'curtin/version.py'
59--- curtin/version.py 1970-01-01 00:00:00 +0000
60+++ curtin/version.py 2017-02-06 22:02:13 +0000
61@@ -0,0 +1,52 @@
62+from curtin import __version__ as old_version
63+import os
64+import subprocess
65+
66+_PACKAGED_VERSION = '@@PACKAGED_VERSION@@'
67+
68+
69+def version_string():
70+ """ Extract a version string from curtin source or version file"""
71+ def _find_path(pathfile):
72+ """ Check for file existance and return dirpath
73+ Search PYTHONPATH, as curtin is typically
74+ launched with PYTHONPATH set, as with curtin pack
75+ executables.
76+ """
77+ if os.path.exists(pathfile):
78+ return os.getcwd()
79+ else:
80+ path = os.path.abspath(os.path.join(
81+ __file__, '..', '..'))
82+ curpath = os.path.join(path, pathfile)
83+ if os.path.exists(curpath):
84+ return os.path.dirname(curpath)
85+
86+ return None
87+
88+ if not _PACKAGED_VERSION.startswith('@@'):
89+ return _PACKAGED_VERSION
90+
91+ bzrdir = _find_path('.bzr')
92+ revno = dpkg_version = None
93+ if bzrdir:
94+ try:
95+ out = subprocess.check_output(['bzr', 'revno'], cwd=bzrdir)
96+ revno = "bzr%s" % out.decode('utf-8').strip()
97+ except subprocess.CalledProcessError:
98+ pass
99+ else:
100+ try:
101+ out = subprocess.check_output(['dpkg-query', '--show',
102+ '--showformat', '${Version}',
103+ 'curtin-common'])
104+ dpkg_version = out.decode('utf-8').strip()
105+ except subprocess.CalledProcessError:
106+ pass
107+ version = old_version
108+ if revno:
109+ version += "~%s" % revno
110+ if dpkg_version:
111+ version = dpkg_version
112+
113+ return version
114
115=== modified file 'debian/rules'
116--- debian/rules 2013-09-20 20:55:05 +0000
117+++ debian/rules 2017-02-06 22:02:13 +0000
118@@ -3,6 +3,10 @@
119 PYVERS := $(shell pyversions -r)
120 PY3VERS := $(shell py3versions -r)
121
122+DEB_VERSION := $(shell dpkg-parsechangelog --show-field=Version)
123+UPSTREAM_VERSION := $(shell x="$(DEB_VERSION)"; echo "$${x%-*}")
124+PKG_VERSION := $(shell x="$(DEB_VERSION)"; echo "$${x\#\#*-}")
125+
126 %:
127 dh $@ --with=python2,python3
128
129@@ -13,3 +17,5 @@
130 $$python setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb; \
131 done
132 chmod 755 $(CURDIR)/debian/tmp/usr/lib/curtin/helpers/*
133+ find $(CURDIR)/debian/tmp
134+ for f in $$(find $(CURDIR)/debian/tmp/usr/lib -type f -name version.py); do [ -f "$$f" ] || continue; sed -i 's,@@PACKAGED_VERSION@@,$(DEB_VERSION),' "$$f"; done
135
136=== added file 'tests/unittests/test_version.py'
137--- tests/unittests/test_version.py 1970-01-01 00:00:00 +0000
138+++ tests/unittests/test_version.py 2017-02-06 22:02:13 +0000
139@@ -0,0 +1,87 @@
140+from unittest import TestCase
141+import mock
142+import subprocess
143+import os
144+
145+from curtin import version
146+from curtin import __version__ as old_version
147+
148+
149+class CurtinVersionBase(TestCase):
150+ def setUp(self):
151+ super(CurtinVersionBase, self).setUp()
152+
153+ def add_patch(self, target, attr):
154+ """Patches specified target object and sets it as attr on test
155+ instance also schedules cleanup"""
156+ m = mock.patch(target, autospec=True)
157+ p = m.start()
158+ self.addCleanup(m.stop)
159+ setattr(self, attr, p)
160+
161+
162+class TestCurtinVersion(CurtinVersionBase):
163+
164+ def setUp(self):
165+ self.add_patch('subprocess.check_output', 'mock_subp')
166+ self.add_patch('os.path', 'mock_path')
167+
168+ @mock.patch.object(os, 'getcwd')
169+ def test_packaged_version(self, mock_getcwd):
170+ original_pkg_string = version._PACKAGED_VERSION
171+ test_pkg_version = '9.8.7-curtin-0ubuntu12'
172+ version._PACKAGED_VERSION = test_pkg_version
173+
174+ ver_string = version.version_string()
175+
176+ # check we got the packaged version string set
177+ self.assertEqual(test_pkg_version, ver_string)
178+ # make sure we didn't take any other path
179+ self.assertEqual([], self.mock_path.call_args_list)
180+ self.assertEqual([], mock_getcwd.call_args_list)
181+ self.assertEqual([], self.mock_subp.call_args_list)
182+
183+ version._PACKAGED_VERSION = original_pkg_string
184+
185+ def test_bzr_revno_version(self):
186+ self.mock_path.exists.return_value = True
187+ bzr_revno = "999"
188+ self.mock_subp.return_value = bzr_revno.encode("utf-8")
189+
190+ ver_string = version.version_string()
191+ self.assertEqual(old_version + "~bzr" + bzr_revno, ver_string)
192+
193+ @mock.patch.object(os, 'getcwd')
194+ def test_bzr_revno_version_exception(self, mock_getcwd):
195+ self.mock_path.exists.return_value = True
196+ mock_getcwd.return_value = "/tmp/foo"
197+ self.mock_subp.side_effect = subprocess.CalledProcessError(1, 'foo')
198+
199+ ver_string = version.version_string()
200+ self.assertEqual(old_version, ver_string)
201+
202+ def test_dpkg_version(self):
203+ self.mock_path.exists.return_value = False
204+ dpkg_version = "7.12.23_curtin_2ubunt29"
205+ self.mock_subp.return_value = dpkg_version.encode("utf-8")
206+
207+ ver_string = version.version_string()
208+ self.assertEqual(dpkg_version, ver_string)
209+
210+ def test_dpkg_version_exception(self):
211+ self.mock_path.exists.return_value = True
212+ self.mock_subp.side_effect = subprocess.CalledProcessError(1, '')
213+
214+ ver_string = version.version_string()
215+ self.assertEqual(old_version, ver_string)
216+
217+ def test_old_version(self):
218+ self.mock_path.exists.return_value = False
219+ self.mock_subp.return_value = "".encode('utf-8')
220+
221+ ver_string = version.version_string()
222+
223+ self.assertEqual(old_version, ver_string)
224+
225+
226+# vi: ts=4 expandtab syntax=python
227
228=== modified file 'tests/vmtests/__init__.py'
229--- tests/vmtests/__init__.py 2017-02-06 20:29:33 +0000
230+++ tests/vmtests/__init__.py 2017-02-06 22:02:13 +0000
231@@ -13,6 +13,7 @@
232 import yaml
233 import curtin.net as curtin_net
234 import curtin.util as util
235+import curtin.version
236
237 from curtin.commands.install import INSTALL_PASS_MSG
238
239@@ -748,6 +749,23 @@
240 with open(os.path.join(self.td.collect, filename), mode) as fp:
241 return fp.read()
242
243+ def load_log_file(self, filename):
244+ with open(filename, 'rb') as l:
245+ return l.read().decode('utf-8', errors='replace')
246+
247+ def get_install_log_curtin_version(self):
248+ # "curtin v. 0.1.0~bzr426 started"
249+ install_log = self.load_log_file(self.install_log)
250+ curtin_vers = [line for line in install_log.splitlines()
251+ if 'curtin v.' in line and line.endswith('started')]
252+ for ver in curtin_vers:
253+ version = ver.split('curtin v. ')[-1].split(' started')[0]
254+ if len(version) > 0:
255+ return version
256+
257+ def get_curtin_version(self):
258+ return curtin.version.version_string()
259+
260 def check_file_strippedline(self, filename, search):
261 lines = self.load_collect_file(filename).splitlines()
262 self.assertIn(search, [i.strip() for i in lines])
263
264=== modified file 'tests/vmtests/test_basic.py'
265--- tests/vmtests/test_basic.py 2016-08-25 23:32:11 +0000
266+++ tests/vmtests/test_basic.py 2017-02-06 22:02:13 +0000
267@@ -122,6 +122,13 @@
268 # no proxy, so the output of apt-config dump should be empty
269 self.assertEqual("", apt_proxy_found)
270
271+ def test_curtin_install_version(self):
272+ installed_version = self.get_install_log_curtin_version()
273+ print('Install log version: %s' % installed_version)
274+ source_version = self.get_curtin_version()
275+ print('Source repo version: %s' % source_version)
276+ self.assertEqual(source_version, installed_version)
277+
278
279 class PreciseTestBasic(relbase.precise, TestBasicAbs):
280 __test__ = True

Subscribers

People subscribed via source and target branches