Merge lp:~didrocks/ubiquity/intall-metrics into lp:ubiquity

Proposed by Didier Roche-Tolomelli
Status: Merged
Merged at revision: 6596
Proposed branch: lp:~didrocks/ubiquity/intall-metrics
Merge into: lp:ubiquity
Diff against target: 390 lines (+150/-21)
8 files modified
debian/changelog (+4/-0)
ubiquity/frontend/debconf_ui.py (+5/-1)
ubiquity/frontend/gtk_ui.py (+8/-1)
ubiquity/frontend/kde_components/PartAuto.py (+7/-5)
ubiquity/frontend/kde_ui.py (+7/-1)
ubiquity/frontend/noninteractive.py (+5/-1)
ubiquity/plugins/ubi-partman.py (+18/-12)
ubiquity/telemetry.py (+96/-0)
To merge this branch: bzr merge lp:~didrocks/ubiquity/intall-metrics
Reviewer Review Type Date Requested Status
Jean-Baptiste Lallement Approve
Iain Lane Approve
Review via email: mp+341229@code.launchpad.net

Description of the change

Add telemetry function

Always report install metrics for optional reporting later on.
Tested on both Kubuntu and Ubuntu, with:
  - side-by-side install
  - use biggest free space
  - reinstall over existing installation, keeping files
  - reinstall and replace existing installation
  - full disk used
  - lvm
  - crypto
  - manual
  - oem
  - automatic installation

It should also work in debconf UI mode, but I'm unsure how to test it.

To post a comment you must log in.
lp:~didrocks/ubiquity/intall-metrics updated
6593. By Jean-Baptiste Lallement

[ Sebastien Bacher ]
  * gui/gtk/stepPrepare.ui:
    - updated the non-free-software text to not include the mention to the
      MP3 fluendo codecs since those have been superseeded in the recent
      gstreamer version.

6594. By Jean-Baptiste Lallement

[ Sebastien Bacher ]
  * Rename the "Preparing to install Ubuntu" installer step to "Updates and
    other software" according to the design, thanks Matthew for pointing out
    the inconsistency

6595. By Jean-Baptiste Lallement

* debian/control: fixed dependency on sensible-utils

Revision history for this message
Iain Lane (laney) wrote :

Thanks, this is great. I have some comments inline, nothing too major. :)

After fixing those, I don't think that you'd need to re-test the full matrix: if it works for one it should work equally for all the others.

Just for the record, as jibel said on IRC, it would be good to test if this works with preseeding. Even if not, that could be fixed later on though.

review: Needs Fixing
Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote :

Waiting for your comments on my answer before going further :)

Revision history for this message
Didier Roche-Tolomelli (didrocks) :
Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote :

ok, sounds like I can't make it a singleton easily without breaking the MVC semantic in place.

I have changed the global as a class variable and made the time measurement relative + adding a "start time"

Let me know what you think about class singleton vs code consistency and "Steps" naming

Revision history for this message
Iain Lane (laney) wrote :

some replies to your comments but I had to do them on the old version of the diff

thanks for considering. As commented I think we should discuss what the requirements for timing data are as this seems a bit strange to me. After getting that settled and maybe adding some protection to the file saving I'm OK with this once jibel has tested and given his thumbs up. Cheers!

Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote :

Done of protecting telemetry. Let's wait on finishing our discussion on what to collect :)

Revision history for this message
Sebastien Bacher (seb128) wrote :

Ok, I've replied on the "recording the steps" comments, those seem like they could be useful to validate design changes and see their impact on the users

Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote :

Tested latest rebased version with the GTK installer, all working!

Revision history for this message
Iain Lane (laney) wrote :

ok from my side, waiting for QA to approve too

review: Approve
Revision history for this message
Jean-Baptiste Lallement (jibel) wrote :

LGTM too. Thanks!

review: Approve
lp:~didrocks/ubiquity/intall-metrics updated
6596. By Jean-Baptiste Lallement

* Added telemetry feature to collect installation metrics and optional reporting later on. Thanks didrocks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'debian/changelog'
--- debian/changelog 2018-03-09 10:43:07 +0000
+++ debian/changelog 2018-03-13 11:08:32 +0000
@@ -12,6 +12,10 @@
12 other software" according to the design, thanks Matthew for pointing 12 other software" according to the design, thanks Matthew for pointing
13 out the inconsistency13 out the inconsistency
1414
15 [ Didier Roche ]
16 * Collect and dump telemetry info as a log file: (LP: #1755456)
17 Those installation telemetry info are for optional upload later on.
18
15 -- Mathieu Trudel-Lapierre <cyphermox@ubuntu.com> Wed, 28 Feb 2018 14:36:54 -050019 -- Mathieu Trudel-Lapierre <cyphermox@ubuntu.com> Wed, 28 Feb 2018 14:36:54 -0500
1620
17ubiquity (18.04.3) bionic; urgency=medium21ubiquity (18.04.3) bionic; urgency=medium
1822
=== modified file 'ubiquity/frontend/debconf_ui.py'
--- ubiquity/frontend/debconf_ui.py 2013-02-19 11:33:07 +0000
+++ ubiquity/frontend/debconf_ui.py 2018-03-13 11:08:32 +0000
@@ -33,7 +33,7 @@
3333
34import debconf34import debconf
3535
36from ubiquity import i18n36from ubiquity import i18n, telemetry
37from ubiquity.components import install, plugininstall37from ubiquity.components import install, plugininstall
38from ubiquity.frontend.base import BaseFrontend, Controller38from ubiquity.frontend.base import BaseFrontend, Controller
39from ubiquity.plugin import Plugin39from ubiquity.plugin import Plugin
@@ -94,6 +94,9 @@
94 'cannot continue without them.'), file=sys.stderr)94 'cannot continue without them.'), file=sys.stderr)
95 sys.exit(1)95 sys.exit(1)
9696
97 telemetry.get().set_installer_type('DebConf')
98 telemetry.get().add_stage(telemetry.START_INSTALL_STAGE_TAG)
99
97 self.pagesindex = 0100 self.pagesindex = 0
98 self.pageslen = 0101 self.pageslen = 0
99 self.pages = []102 self.pages = []
@@ -152,6 +155,7 @@
152 "Install failed with exit code %s; see "155 "Install failed with exit code %s; see "
153 "/var/log/syslog" % ret)156 "/var/log/syslog" % ret)
154157
158 telemetry.get().done(self.db)
155 return 0159 return 0
156 else:160 else:
157 return 10161 return 10
158162
=== modified file 'ubiquity/frontend/gtk_ui.py'
--- ubiquity/frontend/gtk_ui.py 2017-09-28 21:25:09 +0000
+++ ubiquity/frontend/gtk_ui.py 2018-03-13 11:08:32 +0000
@@ -54,7 +54,7 @@
54 from ubiquity import gtkwidgets54 from ubiquity import gtkwidgets
5555
56from ubiquity import (56from ubiquity import (
57 filteredcommand, gsettings, i18n, validation, misc, osextras)57 filteredcommand, gsettings, i18n, validation, misc, osextras, telemetry)
58from ubiquity.components import install, plugininstall, partman_commit58from ubiquity.components import install, plugininstall, partman_commit
59import ubiquity.frontend.base59import ubiquity.frontend.base
60from ubiquity.frontend.base import BaseFrontend60from ubiquity.frontend.base import BaseFrontend
@@ -757,6 +757,8 @@
757 self.debconf_progress_cancellable(False)757 self.debconf_progress_cancellable(False)
758 self.refresh()758 self.refresh()
759759
760 telemetry.get().set_installer_type('GTK')
761
760 self.set_current_page(0)762 self.set_current_page(0)
761 self.live_installer.show()763 self.live_installer.show()
762764
@@ -821,6 +823,8 @@
821 # postinstall will exit here by calling Gtk.main_quit in823 # postinstall will exit here by calling Gtk.main_quit in
822 # find_next_step.824 # find_next_step.
823825
826 telemetry.get().done(self.db)
827
824 self.unlock_environment()828 self.unlock_environment()
825 if self.oem_user_config:829 if self.oem_user_config:
826 self.quit_installer()830 self.quit_installer()
@@ -1555,6 +1559,7 @@
1555 for i in range(len(self.pages))[index + 1:]:1559 for i in range(len(self.pages))[index + 1:]:
1556 self.dot_grid.get_child_at(i, 0).set_fraction(0)1560 self.dot_grid.get_child_at(i, 0).set_fraction(0)
15571561
1562 telemetry.get().add_stage(name)
1558 syslog.syslog('switched to page %s' % name)1563 syslog.syslog('switched to page %s' % name)
15591564
1560 # Callbacks provided to components.1565 # Callbacks provided to components.
@@ -1645,6 +1650,8 @@
1645 return False1650 return False
16461651
1647 def switch_to_install_interface(self):1652 def switch_to_install_interface(self):
1653 if not self.installing:
1654 telemetry.get().add_stage(telemetry.START_INSTALL_STAGE_TAG)
1648 self.installing = True1655 self.installing = True
1649 self.lockdown_environment()1656 self.lockdown_environment()
16501657
16511658
=== modified file 'ubiquity/frontend/kde_components/PartAuto.py'
--- ubiquity/frontend/kde_components/PartAuto.py 2016-08-26 07:13:37 +0000
+++ ubiquity/frontend/kde_components/PartAuto.py 2018-03-13 11:08:32 +0000
@@ -252,18 +252,20 @@
252 disk_id = self.extra_options['use_device'][1][comboText][0]252 disk_id = self.extra_options['use_device'][1][comboText][0]
253 disk_id = disk_id.rsplit('/', 1)[1]253 disk_id = disk_id.rsplit('/', 1)[1]
254 option = self.extra_options['resize'][disk_id][0]254 option = self.extra_options['resize'][disk_id][0]
255 return option, '%d B' % self.resizeSize255 return option, '%d B' % self.resizeSize, 'resize_use_free'
256 elif choice == self.useDeviceChoice:256 elif choice == self.useDeviceChoice:
257 return (self.extra_options['use_device'][0],257 return (self.extra_options['use_device'][0],
258 str(self.part_auto_disk_box.currentText()))258 str(self.part_auto_disk_box.currentText()), 'use_device')
259 elif choice == self.lvm_choice:259 elif choice == self.lvm_choice:
260 return (choice,260 return (choice,
261 str(self.part_auto_disk_box.currentText()))261 str(self.part_auto_disk_box.currentText()), 'use_lvm')
262 elif choice == self.crypto_choice:262 elif choice == self.crypto_choice:
263 return (choice,263 return (choice,
264 str(self.part_auto_disk_box.currentText()))264 str(self.part_auto_disk_box.currentText()), 'use_crypto')
265 elif choice == self.manualChoice:
266 return choice, None, 'manual'
265 else:267 else:
266 return choice, None268 return choice, None, 'unknown'
267269
268 def on_disks_combo_changed(self, index):270 def on_disks_combo_changed(self, index):
269 for e in self.bar_frames:271 for e in self.bar_frames:
270272
=== modified file 'ubiquity/frontend/kde_ui.py'
--- ubiquity/frontend/kde_ui.py 2017-01-09 20:15:33 +0000
+++ ubiquity/frontend/kde_ui.py 2018-03-13 11:08:32 +0000
@@ -41,7 +41,7 @@
41sip.setapi("QVariant", 1)41sip.setapi("QVariant", 1)
42from PyQt5 import QtCore, QtGui, QtWidgets, uic42from PyQt5 import QtCore, QtGui, QtWidgets, uic
4343
44from ubiquity import filteredcommand, i18n, misc44from ubiquity import filteredcommand, i18n, misc, telemetry
45from ubiquity.components import partman_commit, install, plugininstall45from ubiquity.components import partman_commit, install, plugininstall
46import ubiquity.frontend.base46import ubiquity.frontend.base
47from ubiquity.frontend.base import BaseFrontend47from ubiquity.frontend.base import BaseFrontend
@@ -454,6 +454,8 @@
454 self.get_string('ubiquity/install/title'))454 self.get_string('ubiquity/install/title'))
455 self.refresh()455 self.refresh()
456456
457 telemetry.get().set_installer_type('KDE')
458
457 # Start the interface459 # Start the interface
458 self.set_current_page(0)460 self.set_current_page(0)
459461
@@ -529,6 +531,8 @@
529 self.start_slideshow()531 self.start_slideshow()
530 self.run_main_loop()532 self.run_main_loop()
531533
534 telemetry.get().done(self.db)
535
532 quitText = '<qt>%s</qt>' % self.get_string("finished_label")536 quitText = '<qt>%s</qt>' % self.get_string("finished_label")
533 rebootButtonText = self.get_string("reboot_button")537 rebootButtonText = self.get_string("reboot_button")
534 shutdownButtonText = self.get_string("shutdown_button")538 shutdownButtonText = self.get_string("shutdown_button")
@@ -1121,6 +1125,7 @@
1121 self.ui.content_widget.show()1125 self.ui.content_widget.show()
1122 self.current_page = newPageID1126 self.current_page = newPageID
1123 name = self.step_name(newPageID)1127 name = self.step_name(newPageID)
1128 telemetry.get().add_stage(name)
1124 syslog.syslog('switched to page %s' % name)1129 syslog.syslog('switched to page %s' % name)
1125 if 'UBIQUITY_GREETER' in os.environ:1130 if 'UBIQUITY_GREETER' in os.environ:
1126 if name == 'language':1131 if name == 'language':
@@ -1291,6 +1296,7 @@
1291 elif finished_step == 'ubi-partman':1296 elif finished_step == 'ubi-partman':
1292 # Flush changes to the database so that when the parallel db1297 # Flush changes to the database so that when the parallel db
1293 # starts, it does so with the most recent changes.1298 # starts, it does so with the most recent changes.
1299 telemetry.get().add_stage(telemetry.START_INSTALL_STAGE_TAG)
1294 self.stop_debconf()1300 self.stop_debconf()
1295 self.start_debconf()1301 self.start_debconf()
1296 self._show_progress_bar(True)1302 self._show_progress_bar(True)
12971303
=== modified file 'ubiquity/frontend/noninteractive.py'
--- ubiquity/frontend/noninteractive.py 2014-06-30 13:09:16 +0000
+++ ubiquity/frontend/noninteractive.py 2018-03-13 11:08:32 +0000
@@ -32,7 +32,7 @@
3232
33from gi.repository import GLib33from gi.repository import GLib
3434
35from ubiquity import filteredcommand, i18n, misc35from ubiquity import filteredcommand, i18n, misc, telemetry
36from ubiquity.components import install, plugininstall, partman_commit36from ubiquity.components import install, plugininstall, partman_commit
37import ubiquity.frontend.base37import ubiquity.frontend.base
38from ubiquity.frontend.base import BaseFrontend38from ubiquity.frontend.base import BaseFrontend
@@ -77,6 +77,9 @@
77 file=self.console)77 file=self.console)
78 sys.exit(1)78 sys.exit(1)
7979
80 telemetry.get().set_installer_type('NonInteractive')
81 telemetry.get().add_stage(telemetry.START_INSTALL_STAGE_TAG)
82
80 for x in self.pages:83 for x in self.pages:
81 if issubclass(x.filter_class, Plugin):84 if issubclass(x.filter_class, Plugin):
82 ui = x.ui85 ui = x.ui
@@ -110,6 +113,7 @@
110 if ret == 0:113 if ret == 0:
111 self.run_success_cmd()114 self.run_success_cmd()
112 print('Installation complete.', file=self.console)115 print('Installation complete.', file=self.console)
116 telemetry.get().done(self.db)
113 if self.get_reboot():117 if self.get_reboot():
114 misc.execute("reboot")118 misc.execute("reboot")
115 if ret != 0:119 if ret != 0:
116120
=== modified file 'ubiquity/plugins/ubi-partman.py'
--- ubiquity/plugins/ubi-partman.py 2016-12-02 13:20:16 +0000
+++ ubiquity/plugins/ubi-partman.py 2018-03-13 11:08:32 +0000
@@ -25,7 +25,8 @@
2525
26import debconf26import debconf
2727
28from ubiquity import misc, osextras, parted_server, plugin, validation28from ubiquity import (misc, osextras, parted_server, plugin,
29 telemetry, validation)
29from ubiquity.install_misc import archdetect30from ubiquity.install_misc import archdetect
3031
3132
@@ -673,22 +674,24 @@
673674
674 def get_autopartition_choice(self):675 def get_autopartition_choice(self):
675 if self.reuse_partition.get_active():676 if self.reuse_partition.get_active():
676 return self.extra_options['reuse'][0][0], None677 return self.extra_options['reuse'][0][0], None, 'reuse_partition'
677678
678 if self.replace_partition.get_active():679 if self.replace_partition.get_active():
679 return self.extra_options['replace'][0], None680 return (self.extra_options['replace'][0], None,
681 'reinstall_partition')
680682
681 elif self.custom_partitioning.get_active():683 elif self.custom_partitioning.get_active():
682 return self.extra_options['manual'], None684 return self.extra_options['manual'], None, 'manual'
683685
684 elif self.resize_use_free.get_active():686 elif self.resize_use_free.get_active():
685 if 'biggest_free' in self.extra_options:687 if 'biggest_free' in self.extra_options:
686 choice = self.extra_options['biggest_free'][0]688 choice = self.extra_options['biggest_free'][0]
687 return choice, None689 return choice, None, 'resize_use_free'
688 else:690 else:
689 disk_id = self.get_current_disk_partman_id()691 disk_id = self.get_current_disk_partman_id()
690 choice = self.extra_options['resize'][disk_id][0]692 choice = self.extra_options['resize'][disk_id][0]
691 return choice, '%s B' % self.resizewidget.get_size()693 return (choice, '%s B' % self.resizewidget.get_size(),
694 'resize_use_free')
692695
693 elif self.use_device.get_active():696 elif self.use_device.get_active():
694 def choose_recipe():697 def choose_recipe():
@@ -702,13 +705,15 @@
702705
703 if not ((want_crypto and have_crypto) or706 if not ((want_crypto and have_crypto) or
704 (want_lvm and have_lvm)):707 (want_lvm and have_lvm)):
705 return self.extra_options['use_device'][0]708 return self.extra_options['use_device'][0], 'use_device'
706709
707 if want_crypto:710 if want_crypto:
708 return self.extra_options['some_device_crypto']711 return (self.extra_options['some_device_crypto'],
712 'use_crypto')
709713
710 if want_lvm:714 if want_lvm:
711 return self.extra_options['some_device_lvm']715 return (self.extra_options['some_device_lvm'],
716 'use_lvm')
712717
713 # Something went horribly wrong, we should have returned718 # Something went horribly wrong, we should have returned
714 # earlier719 # earlier
@@ -717,9 +722,9 @@
717 i = self.part_auto_select_drive.get_active_iter()722 i = self.part_auto_select_drive.get_active_iter()
718 m = self.part_auto_select_drive.get_model()723 m = self.part_auto_select_drive.get_model()
719 disk = m.get_value(i, 0)724 disk = m.get_value(i, 0)
720 choice = choose_recipe()725 choice, method = choose_recipe()
721 # Is the encoding necessary?726 # Is the encoding necessary?
722 return choice, misc.utf8(disk, errors='replace')727 return choice, misc.utf8(disk, errors='replace'), method
723728
724 else:729 else:
725 raise AssertionError("Couldn't get autopartition choice")730 raise AssertionError("Couldn't get autopartition choice")
@@ -3249,10 +3254,11 @@
3249 self.preseed('grub-installer/bootdev', self.ui.get_grub_choice())3254 self.preseed('grub-installer/bootdev', self.ui.get_grub_choice())
32503255
3251 if self.current_question.endswith('automatically_partition'):3256 if self.current_question.endswith('automatically_partition'):
3252 (autopartition_choice, self.extra_choice) = \3257 (autopartition_choice, self.extra_choice, method) = \
3253 self.ui.get_autopartition_choice()3258 self.ui.get_autopartition_choice()
3254 self.preseed_as_c(self.current_question, autopartition_choice,3259 self.preseed_as_c(self.current_question, autopartition_choice,
3255 seen=False)3260 seen=False)
3261 telemetry.get().set_partition_method(method)
3256 # Don't exit partman yet.3262 # Don't exit partman yet.
3257 else:3263 else:
3258 self.finish_partitioning = True3264 self.finish_partitioning = True
32593265
=== added file 'ubiquity/telemetry.py'
--- ubiquity/telemetry.py 1970-01-01 00:00:00 +0000
+++ ubiquity/telemetry.py 2018-03-13 11:08:32 +0000
@@ -0,0 +1,96 @@
1# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
2
3# Copyright (C) 2018 Canonical Ltd.
4#
5# Functions useful for the final install.py script and for ubiquity
6# plugins to use
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21
22
23import json
24import os
25import stat
26import syslog
27import time
28
29from ubiquity.misc import raise_privileges
30
31START_INSTALL_STAGE_TAG = 'start_install'
32
33
34def get():
35 """Return a singleton _Telemetry instance."""
36 if _Telemetry._telemetry is None:
37 _Telemetry._telemetry = _Telemetry()
38 return _Telemetry._telemetry
39
40
41class _Telemetry():
42
43 _telemetry = None
44
45 def __init__(self):
46 self._metrics = {}
47 self._stages_hist = {}
48 self._start_time = time.time()
49 self.add_stage('start')
50 self._dest_path = '/target/var/log/installer/telemetry'
51 try:
52 with open('/cdrom/.disk/info') as f:
53 self._metrics['Media'] = f.readline()
54 except FileNotFoundError:
55 self._metrics['Media'] = 'unknown'
56
57 def add_stage(self, stage_name):
58 """Record installer stage with current time"""
59 self._stages_hist[int(time.time() - self._start_time)] = stage_name
60
61 def set_installer_type(self, installer_type):
62 """Record installer type"""
63 self._metrics['Type'] = installer_type
64
65 def set_partition_method(self, method):
66 """Record anynomized partition method"""
67 self._metrics['PartitionMethod'] = method
68
69 @raise_privileges
70 def done(self, db):
71 """Close telemetry collection
72
73 Set as installation done, add additional info and save to
74 destination file"""
75 self.add_stage('done')
76
77 self._metrics['DownloadUpdates'] = db.get('ubiquity/download_updates')
78 self._metrics['Language'] = db.get('localechooser/languagelist')
79 self._metrics['Minimal'] = db.get('ubiquity/minimal_install')
80 self._metrics['RestrictedAddons'] = db.get('ubiquity/use_nonfree')
81 self._metrics['Stages'] = self._stages_hist
82
83 target_dir = os.path.dirname(self._dest_path)
84 try:
85 if not os.path.exists(target_dir):
86 os.makedirs(target_dir)
87 with open(self._dest_path, 'w') as f:
88 json.dump(self._metrics, f)
89 os.chmod(self._dest_path,
90 stat.S_IRUSR | stat.S_IWUSR |
91 stat.S_IRGRP | stat.S_IROTH)
92 except OSError as e:
93 syslog.syslog(syslog.LOG_ERR,
94 "Exception while storing telemetry data: " + str(e))
95
96# vim:ai:et:sts=4:tw=80:sw=4:

Subscribers

People subscribed via source and target branches

to status/vote changes: