~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/hardwaredb/scripts/hwdbsubmissions.py

Undo rename. Again.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
3
 
4
4
"""Parse Hardware Database submissions.
11
11
__all__ = [
12
12
           'SubmissionParser',
13
13
           'process_pending_submissions',
14
 
           'ProcessingLoopForPendingSubmissions',
15
 
           'ProcessingLoopForReprocessingBadSubmissions',
16
14
          ]
17
15
 
18
16
import bz2
34
32
 
35
33
from zope.component import getUtility
36
34
from zope.interface import implements
37
 
from zope.security.proxy import removeSecurityProxy
38
35
 
39
36
from canonical.lazr.xml import RelaxNGValidator
40
37
 
52
49
    IHWVendorIDSet,
53
50
    IHWVendorNameSet,
54
51
    )
55
 
from lp.hardwaredb.model.hwdb import HWSubmission
56
52
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
57
53
from canonical.launchpad.interfaces.looptuner import ITunableLoop
58
54
from canonical.launchpad.utilities.looptuner import LoopTuner
75
71
    """,
76
72
    re.VERBOSE)
77
73
 
78
 
_broken_comment_nodes_re = re.compile('(<comment>.*?</comment>)', re.DOTALL)
79
 
_missing_udev_node_data = re.compile(
80
 
    '<info command="udevadm info --export-db">(.*?)</info>', re.DOTALL)
81
 
_missing_dmi_node_data = re.compile(
82
 
    r'<info command="grep -r \. /sys/class/dmi/id/ 2&gt;/dev/null">(.*?)'
83
 
    '</info>', re.DOTALL)
84
 
_udev_node_exists = re.compile('<hardware>.*?<udev>.*?</hardware>', re.DOTALL)
85
 
_dmi_node_exists = re.compile('<hardware>.*?<dmi>.*?</hardware>', re.DOTALL)
86
 
 
87
74
ROOT_UDI = '/org/freedesktop/Hal/devices/computer'
88
75
UDEV_ROOT_PATH = '/devices/LNXSYSTM:00'
89
76
 
129
116
UDEV_USB_TYPE_RE = re.compile('^[0-9]{1,3}/[0-9]{1,3}/[0-9]{1,3}$')
130
117
SYSFS_SCSI_DEVICE_ATTRIBUTES = set(('vendor', 'model', 'type'))
131
118
 
132
 
 
133
119
class SubmissionParser(object):
134
120
    """A Parser for the submissions to the hardware database."""
135
121
 
136
 
    def __init__(self, logger=None, record_warnings=True):
 
122
    def __init__(self, logger=None):
137
123
        if logger is None:
138
124
            logger = getLogger()
139
125
        self.logger = logger
149
135
        self._setMainSectionParsers()
150
136
        self._setHardwareSectionParsers()
151
137
        self._setSoftwareSectionParsers()
152
 
        self.record_warnings = record_warnings
153
138
 
154
139
    def _logError(self, message, submission_key, create_oops=True):
155
140
        """Log `message` for an error in submission submission_key`."""
162
147
 
163
148
    def _logWarning(self, message, warning_id=None):
164
149
        """Log `message` for a warning in submission submission_key`."""
165
 
        if not self.record_warnings:
166
 
            return
167
150
        if warning_id is None:
168
151
            issue_warning = True
169
152
        elif warning_id not in self._logged_warnings:
175
158
            self.logger.warning(
176
159
                'Parsing submission %s: %s' % (self.submission_key, message))
177
160
 
178
 
    def fixFrequentErrors(self, submission):
179
 
        """Fixes for frequent formal errors in the submissions.
180
 
        """
181
 
        # A considerable number of reports for Lucid has ESC characters
182
 
        # in comment nodes. We don't need the comment nodes at all, so
183
 
        # we can simply empty them.
184
 
        submission = _broken_comment_nodes_re.sub('<comment/>', submission)
185
 
 
186
 
        # Submissions from Natty don't have the nodes <dmi> and <udev>
187
 
        # as children of the <hardware> node. Fortunately, they provide
188
 
        # this data in
189
 
        #
190
 
        #    <context>
191
 
        #        <info command="grep -r . /sys/class/dmi/id/ 2&gt;/dev/null">
192
 
        #        ...
193
 
        #        </info>
194
 
        #        <info command="udevadm info --export-db">
195
 
        #        ...
196
 
        #        </info>
197
 
        #    </context>
198
 
        #
199
 
        # We can try to find the two relevant <info> nodes inside <context>
200
 
        # and move their content into the proper subnodes of <hardware>.
201
 
        if _udev_node_exists.search(submission) is None:
202
 
            mo = _missing_udev_node_data.search(submission)
203
 
            if mo is not None:
204
 
                missing_data = mo.group(1)
205
 
                missing_data = '<udev>%s</udev>\n</hardware>' % missing_data
206
 
                submission = submission.replace('</hardware>', missing_data)
207
 
        if _dmi_node_exists.search(submission) is None:
208
 
            mo = _missing_dmi_node_data.search(submission)
209
 
            if mo is not None:
210
 
                missing_data = mo.group(1)
211
 
                missing_data = '<dmi>%s</dmi>\n</hardware>' % missing_data
212
 
                submission = submission.replace('</hardware>', missing_data)
213
 
        return submission
214
 
 
215
161
    def _getValidatedEtree(self, submission, submission_key):
216
162
        """Create an etree doc from the XML string submission and validate it.
217
163
 
218
164
        :return: an `lxml.etree` instance representation of a valid
219
165
            submission or None for invalid submissions.
220
166
        """
221
 
        submission = self.fixFrequentErrors(submission)
222
167
        try:
223
168
            tree = etree.parse(StringIO(submission), parser=self.doc_parser)
224
169
        except SyntaxError, error_value:
406
351
 
407
352
        :return: (name, (value, type)) of a property.
408
353
        """
 
354
        property_name = property_node.get('name')
409
355
        return (property_node.get('name'),
410
356
                self._getValueAndType(property_node))
411
357
 
734
680
                 where the values are the parsing results of _parseHAL,
735
681
                 _parseProcessors, _parseAliases.
736
682
        """
737
 
        # Submissions from checkbox for Lucid, Maverick and Natty
738
 
        # unfortunately do not contain a <sysfs-attributes> node.
739
 
        # A default value here allows us to mark these submissions.
740
 
        # See also bug 835103.
741
 
        hardware_data = {
742
 
            'sysfs-attributes': None,
743
 
            }
 
683
        hardware_data = {}
744
684
        for node in hardware_node.getchildren():
745
685
            parser = self._parse_hardware_section[node.tag]
746
686
            result = parser(node)
1004
944
                 the content.
1005
945
        """
1006
946
        self.submission_key = submission_key
1007
 
        submission_doc = self._getValidatedEtree(submission, submission_key)
 
947
        submission_doc  = self._getValidatedEtree(submission, submission_key)
1008
948
        if submission_doc is None:
1009
949
            return None
1010
950
 
1388
1328
        should have a corresponding sysfs node, and this node should
1389
1329
        define the attributes 'vendor', 'model', 'type'.
1390
1330
        """
1391
 
        # Broken submissions from Lucid, Maverick and Natty. We'll have
1392
 
        # to deal with incomplete data for SCSI devices in this case if
1393
 
        # we don't want to drop the entire submission, so just pretend
1394
 
        # that things are fine.
1395
 
        # See also bug 835103.
1396
 
        if sysfs_data is None:
1397
 
            return True
1398
1331
        for device in udev_data:
1399
1332
            subsystem = device['E'].get('SUBSYSTEM')
1400
1333
            if subsystem != 'scsi':
1523
1456
        dmi_data = parsed_data['hardware']['dmi']
1524
1457
        for udev_data in parsed_data['hardware']['udev']:
1525
1458
            device_path = udev_data['P']
1526
 
            if sysfs_data is not None:
1527
 
                sysfs_data_for_device = sysfs_data.get(device_path)
1528
 
            else:
1529
 
                # broken Lucid, Maverick and Natty submissions.
1530
 
                # See also bug 835103.
1531
 
                sysfs_data_for_device = None
1532
1459
            if device_path == UDEV_ROOT_PATH:
1533
1460
                device = UdevDevice(
1534
 
                    self, udev_data, sysfs_data=sysfs_data_for_device,
 
1461
                    self, udev_data, sysfs_data=sysfs_data.get(device_path),
1535
1462
                    dmi_data=dmi_data)
1536
1463
            else:
1537
1464
                device = UdevDevice(
1538
 
                    self, udev_data, sysfs_data=sysfs_data_for_device)
 
1465
                    self, udev_data, sysfs_data=sysfs_data.get(device_path))
1539
1466
            self.devices[device_path] = device
1540
1467
 
1541
1468
        # The parent-child relations are derived from the path names of
1565
1492
                    'Invalid device path name: %r' % path_name,
1566
1493
                    self.submission_key)
1567
1494
                return False
1568
 
            for parent_path in path_names[path_index + 1:]:
 
1495
            for parent_path in path_names[path_index+1:]:
1569
1496
                if path_name.startswith(parent_path):
1570
1497
                    self.devices[parent_path].addChild(
1571
1498
                        self.devices[path_name])
1693
1620
        0: HWBus.SCSI,
1694
1621
        1: HWBus.IDE,
1695
1622
        2: HWBus.FLOPPY,
1696
 
        3: HWBus.IPI,  # Intelligent Peripheral Interface.
 
1623
        3: HWBus.IPI, # Intelligent Peripheral Interface
1697
1624
        5: HWBus.ATA,
1698
1625
        6: HWBus.SATA,
1699
1626
        7: HWBus.SAS,
2804
2731
        # udev sets the property SUBSYSTEM to "scsi" for a number of
2805
2732
        # different nodes: SCSI hosts, SCSI targets and SCSI devices.
2806
2733
        # They are distiguished by the property DEVTYPE.
2807
 
 
2808
 
        # Hack for broken submissions from Lucid, Maverick and Natty:
2809
 
        # If we don't have sysfs information, pretend that no SCSI
2810
 
        # related node corresponds to a real device.
2811
 
        # See also bug 835103.
2812
 
        if self.sysfs is None:
2813
 
            return False
2814
2734
        properties = self.udev['E']
2815
2735
        return (properties.get('SUBSYSTEM') == 'scsi' and
2816
2736
                properties.get('DEVTYPE') == 'scsi_device')
2822
2742
            # SubmissionParser.checkUdevScsiProperties() ensures that
2823
2743
            # each SCSI device has a record in self.sysfs and that
2824
2744
            # the attribute 'vendor' exists.
 
2745
            path = self.udev['P']
2825
2746
            return self.sysfs['vendor']
2826
2747
        else:
2827
2748
            return None
2833
2754
            # SubmissionParser.checkUdevScsiProperties() ensures that
2834
2755
            # each SCSI device has a record in self.sysfs and that
2835
2756
            # the attribute 'model' exists.
 
2757
            path = self.udev['P']
2836
2758
            return self.sysfs['model']
2837
2759
        else:
2838
2760
            return None
2977
2899
        return self.udev['id']
2978
2900
 
2979
2901
 
2980
 
class ProcessingLoopBase(object):
 
2902
class ProcessingLoop(object):
2981
2903
    """An `ITunableLoop` for processing HWDB submissions."""
2982
2904
 
2983
2905
    implements(ITunableLoop)
2984
2906
 
2985
 
    def __init__(self, transaction, logger, max_submissions, record_warnings):
 
2907
    def __init__(self, transaction, logger, max_submissions):
2986
2908
        self.transaction = transaction
2987
2909
        self.logger = logger
2988
2910
        self.max_submissions = max_submissions
2990
2912
        self.invalid_submissions = 0
2991
2913
        self.finished = False
2992
2914
        self.janitor = getUtility(ILaunchpadCelebrities).janitor
2993
 
        self.record_warnings = record_warnings
2994
2915
 
2995
2916
    def _validateSubmission(self, submission):
2996
2917
        submission.status = HWSubmissionProcessingStatus.PROCESSED
3013
2934
        error_utility.raising(info, request)
3014
2935
        self.logger.error('%s (%s)' % (error_explanation, request.oopsid))
3015
2936
 
3016
 
    def getUnprocessedSubmissions(self, chunk_size):
3017
 
        raise NotImplementedError
3018
 
 
3019
2937
    def __call__(self, chunk_size):
3020
2938
        """Process a batch of yet unprocessed HWDB submissions."""
3021
2939
        # chunk_size is a float; we compare it below with an int value,
3022
2940
        # which can lead to unexpected results. Since it is also used as
3023
2941
        # a limit for an SQL query, convert it into an integer.
3024
2942
        chunk_size = int(chunk_size)
3025
 
        submissions = self.getUnprocessedSubmissions(chunk_size)
 
2943
        submissions = getUtility(IHWSubmissionSet).getByStatus(
 
2944
            HWSubmissionProcessingStatus.SUBMITTED,
 
2945
            user=self.janitor
 
2946
            )[:chunk_size]
3026
2947
        # Listify the submissions, since we'll have to loop over each
3027
2948
        # one anyway. This saves a COUNT query for getting the number of
3028
2949
        # submissions
3039
2960
        # loop.
3040
2961
        for submission in submissions:
3041
2962
            try:
3042
 
                parser = SubmissionParser(self.logger, self.record_warnings)
 
2963
                parser = SubmissionParser(self.logger)
3043
2964
                success = parser.processSubmission(submission)
3044
2965
                if success:
3045
2966
                    self._validateSubmission(submission)
3048
2969
            except (KeyboardInterrupt, SystemExit):
3049
2970
                # We should never catch these exceptions.
3050
2971
                raise
3051
 
            except LibrarianServerError:
 
2972
            except LibrarianServerError, error:
3052
2973
                # LibrarianServerError is raised when the server could
3053
2974
                # not be reaches for 30 minutes.
3054
2975
                #
3067
2988
                    'Could not reach the Librarian while processing HWDB '
3068
2989
                    'submission %s' % submission.submission_key)
3069
2990
                raise
3070
 
            except Exception:
 
2991
            except Exception, error:
3071
2992
                self.transaction.abort()
3072
2993
                self.reportOops(
3073
2994
                    'Exception while processing HWDB submission %s'
3078
2999
                # further submissions in this batch raise an exception.
3079
3000
                self.transaction.commit()
3080
3001
 
3081
 
            self.start = submission.id + 1
3082
3002
            if self.max_submissions is not None:
3083
3003
                if self.max_submissions <= (
3084
3004
                    self.valid_submissions + self.invalid_submissions):
3086
3006
                    break
3087
3007
        self.transaction.commit()
3088
3008
 
3089
 
 
3090
 
class ProcessingLoopForPendingSubmissions(ProcessingLoopBase):
3091
 
 
3092
 
    def getUnprocessedSubmissions(self, chunk_size):
3093
 
        submissions = getUtility(IHWSubmissionSet).getByStatus(
3094
 
            HWSubmissionProcessingStatus.SUBMITTED,
3095
 
            user=self.janitor
3096
 
            )[:chunk_size]
3097
 
        submissions = list(submissions)
3098
 
        return submissions
3099
 
 
3100
 
 
3101
 
class ProcessingLoopForReprocessingBadSubmissions(ProcessingLoopBase):
3102
 
 
3103
 
    def __init__(self, start, transaction, logger,
3104
 
                 max_submissions, record_warnings):
3105
 
        super(ProcessingLoopForReprocessingBadSubmissions, self).__init__(
3106
 
            transaction, logger, max_submissions, record_warnings)
3107
 
        self.start = start
3108
 
 
3109
 
    def getUnprocessedSubmissions(self, chunk_size):
3110
 
        submissions = getUtility(IHWSubmissionSet).getByStatus(
3111
 
            HWSubmissionProcessingStatus.INVALID, user=self.janitor)
3112
 
        submissions = removeSecurityProxy(submissions).find(
3113
 
            HWSubmission.id >= self.start)
3114
 
        submissions = list(submissions[:chunk_size])
3115
 
        return submissions
3116
 
 
3117
 
 
3118
 
def process_pending_submissions(transaction, logger, max_submissions=None,
3119
 
                                record_warnings=True):
 
3009
def process_pending_submissions(transaction, logger, max_submissions=None):
3120
3010
    """Process pending submissions.
3121
3011
 
3122
3012
    Parse pending submissions, store extracted data in HWDB tables and
3123
3013
    mark them as either PROCESSED or INVALID.
3124
3014
    """
3125
 
    loop = ProcessingLoopForPendingSubmissions(
3126
 
        transaction, logger, max_submissions, record_warnings)
3127
 
    # It is hard to predict how long it will take to parse a submission.
3128
 
    # we don't want to last a DB transaction too long but we also
3129
 
    # don't want to commit more often than necessary. The LoopTuner
3130
 
    # handles this for us. The loop's run time will be approximated to
3131
 
    # 2 seconds, but will never handle more than 50 submissions.
3132
 
    loop_tuner = LoopTuner(
3133
 
                loop, 2, minimum_chunk_size=1, maximum_chunk_size=50)
3134
 
    loop_tuner.run()
3135
 
    logger.info(
3136
 
        'Processed %i valid and %i invalid HWDB submissions'
3137
 
        % (loop.valid_submissions, loop.invalid_submissions))
3138
 
 
3139
 
 
3140
 
def reprocess_invalid_submissions(start, transaction, logger,
3141
 
                                  max_submissions=None, record_warnings=True):
3142
 
    """Reprocess invalid submissions.
3143
 
 
3144
 
    Parse submissions that have been marked as invalid. A newer
3145
 
    variant of the parser might be able to process them.
3146
 
    """
3147
 
    loop = ProcessingLoopForReprocessingBadSubmissions(
3148
 
        start, transaction, logger, max_submissions, record_warnings)
3149
 
    # It is hard to predict how long it will take to parse a submission.
3150
 
    # we don't want to last a DB transaction too long but we also
3151
 
    # don't want to commit more often than necessary. The LoopTuner
3152
 
    # handles this for us. The loop's run time will be approximated to
3153
 
    # 2 seconds, but will never handle more than 50 submissions.
3154
 
    loop_tuner = LoopTuner(
3155
 
                loop, 2, minimum_chunk_size=1, maximum_chunk_size=50)
3156
 
    loop_tuner.run()
3157
 
    logger.info(
3158
 
        'Processed %i valid and %i invalid HWDB submissions'
3159
 
        % (loop.valid_submissions, loop.invalid_submissions))
3160
 
    logger.info('last processed: %i' % loop.start)
3161
 
    return loop.start
 
3015
    loop = ProcessingLoop(transaction, logger, max_submissions)
 
3016
    # It is hard to predict how long it will take to parse a submission.
 
3017
    # we don't want to last a DB transaction too long but we also
 
3018
    # don't want to commit more often than necessary. The LoopTuner
 
3019
    # handles this for us. The loop's run time will be approximated to
 
3020
    # 2 seconds, but will never handle more than 50 submissions.
 
3021
    loop_tuner = LoopTuner(
 
3022
                loop, 2, minimum_chunk_size=1, maximum_chunk_size=50)
 
3023
    loop_tuner.run()
 
3024
    logger.info(
 
3025
        'Processed %i valid and %i invalid HWDB submissions'
 
3026
        % (loop.valid_submissions, loop.invalid_submissions))