VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py@ 66998

Last change on this file since 66998 was 65980, checked in by vboxsync, 8 years ago

testmanager/core: pylint 2.0.0 fixes.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 24.7 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: failurereason.py 65980 2017-03-07 13:00:36Z vboxsync $
3
4"""
5Test Manager - Failure Reasons.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2016 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 65980 $"
30
31
32# Validation Kit imports.
33from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
34 AttributeChangeEntry, ChangeLogEntry;
35from testmanager.core.useraccount import UserAccountLogic;
36
37
38
39class FailureReasonData(ModelDataBase):
40 """
41 Failure Reason Data.
42 """
43
44 ksIdAttr = 'idFailureReason';
45
46 ksParam_idFailureReason = 'FailureReasonData_idFailureReason'
47 ksParam_tsEffective = 'FailureReasonData_tsEffective'
48 ksParam_tsExpire = 'FailureReasonData_tsExpire'
49 ksParam_uidAuthor = 'FailureReasonData_uidAuthor'
50 ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory'
51 ksParam_sShort = 'FailureReasonData_sShort'
52 ksParam_sFull = 'FailureReasonData_sFull'
53 ksParam_iTicket = 'FailureReasonData_iTicket'
54 ksParam_asUrls = 'FailureReasonData_asUrls'
55
56 kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire',
57 'uidAuthor', 'iTicket', 'asUrls' ]
58
59 def __init__(self):
60 ModelDataBase.__init__(self);
61
62 #
63 # Initialize with defaults.
64 # See the database for explanations of each of these fields.
65 #
66
67 self.idFailureReason = None
68 self.tsEffective = None
69 self.tsExpire = None
70 self.uidAuthor = None
71 self.idFailureCategory = None
72 self.sShort = None
73 self.sFull = None
74 self.iTicket = None
75 self.asUrls = None
76
77 def initFromDbRow(self, aoRow):
78 """
79 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
80
81 Returns self. Raises exception if the row is None or otherwise invalid.
82 """
83
84 if aoRow is None:
85 raise TMRowNotFound('Failure Reason not found.');
86
87 self.idFailureReason = aoRow[0]
88 self.tsEffective = aoRow[1]
89 self.tsExpire = aoRow[2]
90 self.uidAuthor = aoRow[3]
91 self.idFailureCategory = aoRow[4]
92 self.sShort = aoRow[5]
93 self.sFull = aoRow[6]
94 self.iTicket = aoRow[7]
95 self.asUrls = aoRow[8]
96
97 return self;
98
99 def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None):
100 """
101 Initialize from the database, given the ID of a row.
102 """
103 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
104 'SELECT *\n'
105 'FROM FailureReasons\n'
106 'WHERE idFailureReason = %s\n'
107 , ( idFailureReason,), tsNow, sPeriodBack));
108 aoRow = oDb.fetchOne()
109 if aoRow is None:
110 raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)'
111 % (idFailureReason, tsNow, sPeriodBack,));
112 return self.initFromDbRow(aoRow);
113
114
115class FailureReasonDataEx(FailureReasonData):
116 """
117 Failure Reason Data, extended version that includes the category.
118 """
119
120 def __init__(self):
121 FailureReasonData.__init__(self);
122 self.oCategory = None;
123 self.oAuthor = None;
124
125 def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
126 """
127 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
128
129 Returns self. Raises exception if the row is None or otherwise invalid.
130 """
131
132 self.initFromDbRow(aoRow);
133 self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory);
134 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
135
136 return self;
137
138
139class FailureReasonLogic(ModelLogicBase): # pylint: disable=R0903
140 """
141 Failure Reason logic.
142 """
143
144 def __init__(self, oDb):
145 ModelLogicBase.__init__(self, oDb)
146 self.dCache = None;
147 self.dCacheNameAndCat = None;
148 self.oCategoryLogic = None;
149 self.oUserAccountLogic = None;
150
151 def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
152 """
153 Fetches Failure Category records.
154
155 Returns an array (list) of FailureReasonDataEx items, empty list if none.
156 Raises exception on error.
157 """
158 _ = aiSortColumns;
159 self._ensureCachesPresent();
160
161 if tsNow is None:
162 self._oDb.execute('SELECT FailureReasons.*,\n'
163 ' FailureCategories.sShort AS sCategory\n'
164 'FROM FailureReasons,\n'
165 ' FailureCategories\n'
166 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
167 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
168 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
169 'ORDER BY sCategory ASC, sShort ASC\n'
170 'LIMIT %s OFFSET %s\n'
171 , (cMaxRows, iStart,));
172 else:
173 self._oDb.execute('SELECT FailureReasons.*,\n'
174 ' FailureCategories.sShort AS sCategory\n'
175 'FROM FailureReasons,\n'
176 ' FailureCategories\n'
177 'WHERE FailureReasons.tsExpire > %s\n'
178 ' AND FailureReasons.tsEffective <= %s\n'
179 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
180 ' AND FailureReasons.tsExpire > %s\n'
181 ' AND FailureReasons.tsEffective <= %s\n'
182 'ORDER BY sCategory ASC, sShort ASC\n'
183 'LIMIT %s OFFSET %s\n'
184 , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
185
186 aoRows = []
187 for aoRow in self._oDb.fetchAll():
188 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
189 return aoRows
190
191 def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None):
192 """
193 Fetches Failure Category records.
194
195 Returns an array (list) of FailureReasonDataEx items, empty list if none.
196 Raises exception on error.
197 """
198 _ = aiSortColumns;
199 self._ensureCachesPresent();
200
201 if tsNow is None:
202 self._oDb.execute('SELECT *\n'
203 'FROM FailureReasons\n'
204 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
205 ' AND idFailureCategory = %s\n'
206 'ORDER BY sShort ASC\n'
207 'LIMIT %s OFFSET %s\n'
208 , ( idFailureCategory, cMaxRows, iStart,));
209 else:
210 self._oDb.execute('SELECT *\n'
211 'FROM FailureReasons\n'
212 'WHERE idFailureCategory = %s\n'
213 ' AND tsExpire > %s\n'
214 ' AND tsEffective <= %s\n'
215 'ORDER BY sShort ASC\n'
216 'LIMIT %s OFFSET %s\n'
217 , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,));
218
219 aoRows = []
220 for aoRow in self._oDb.fetchAll():
221 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
222 return aoRows
223
224
225 def fetchForSheriffByNamedCategory(self, sFailureCategory):
226 """
227 Fetches the short names of the reasons in the named category.
228
229 Returns array of strings.
230 Raises exception on error.
231 """
232 self._oDb.execute('SELECT FailureReasons.sShort\n'
233 'FROM FailureReasons,\n'
234 ' FailureCategories\n'
235 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
236 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
237 ' AND FailureCategories.sShort = %s\n'
238 'ORDER BY FailureReasons.sShort ASC\n'
239 , ( sFailureCategory,));
240 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
241
242
243 def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
244 """
245 Gets the list of Failure Reasons for a combo box.
246 Returns an array of (value [idFailureReason], drop-down-name [sShort],
247 hover-text [sFull]) tuples.
248 """
249 if tsEffective is None:
250 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
251 'FROM FailureReasons fr,\n'
252 ' FailureCategories fc\n'
253 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
254 ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n'
255 ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n'
256 'ORDER BY sComboText')
257 else:
258 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
259 'FROM FailureReasons fr,\n'
260 ' FailureCategories fc\n'
261 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
262 ' AND fr.tsExpire > %s\n'
263 ' AND fr.tsEffective <= %s\n'
264 ' AND fc.tsExpire > %s\n'
265 ' AND fc.tsEffective <= %s\n'
266 'ORDER BY sComboText'
267 , (tsEffective, tsEffective, tsEffective, tsEffective));
268 aoRows = self._oDb.fetchAll();
269 return [(-1, sFirstEntry, '')] + aoRows;
270
271
272 def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=R0914
273 """
274 Fetches change log entries for a failure reason.
275
276 Returns an array of ChangeLogEntry instance and an indicator whether
277 there are more entries.
278 Raises exception on error.
279 """
280 self._ensureCachesPresent();
281
282 if tsNow is None:
283 tsNow = self._oDb.getCurrentTimestamp();
284
285 # 1. Get a list of the relevant changes.
286 self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
287 'ORDER BY tsEffective DESC\n'
288 'LIMIT %s OFFSET %s\n'
289 , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
290 aoRows = [];
291 for aoChange in self._oDb.fetchAll():
292 aoRows.append(FailureReasonData().initFromDbRow(aoChange));
293
294 # 2. Calculate the changes.
295 aoEntries = [];
296 for i in xrange(0, len(aoRows) - 1):
297 oNew = aoRows[i];
298 oOld = aoRows[i + 1];
299
300 aoChanges = [];
301 for sAttr in oNew.getDataAttributes():
302 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
303 oOldAttr = getattr(oOld, sAttr);
304 oNewAttr = getattr(oNew, sAttr);
305 if oOldAttr != oNewAttr:
306 if sAttr == 'idFailureCategory':
307 oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
308 if oCat is not None:
309 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
310 oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
311 if oCat is not None:
312 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
313 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
314
315 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
316
317 # If we're at the end of the log, add the initial entry.
318 if len(aoRows) <= cMaxRows and aoRows:
319 oNew = aoRows[-1];
320 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
321
322 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
323
324
325 def getById(self, idFailureReason):
326 """Get Failure Reason data by idFailureReason"""
327
328 self._oDb.execute('SELECT *\n'
329 'FROM FailureReasons\n'
330 'WHERE tsExpire = \'infinity\'::timestamp\n'
331 ' AND idFailureReason = %s;', (idFailureReason,))
332 aRows = self._oDb.fetchAll()
333 if len(aRows) not in (0, 1):
334 raise self._oDb.integrityException(
335 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
336 try:
337 return FailureReasonData().initFromDbRow(aRows[0])
338 except IndexError:
339 return None
340
341
342 def addEntry(self, oData, uidAuthor, fCommit = False):
343 """
344 Add a failure reason.
345 """
346 #
347 # Validate.
348 #
349 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
350 if dErrors:
351 raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
352
353 #
354 # Add the record.
355 #
356 self._readdEntry(uidAuthor, oData);
357 self._oDb.maybeCommit(fCommit);
358 return True;
359
360
361 def editEntry(self, oData, uidAuthor, fCommit = False):
362 """
363 Modifies a failure reason.
364 """
365
366 #
367 # Validate inputs and read in the old(/current) data.
368 #
369 assert isinstance(oData, FailureReasonData);
370 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
371 if dErrors:
372 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
373
374 oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason);
375
376 #
377 # Update the data that needs updating.
378 #
379 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
380 self._historizeEntry(oData.idFailureReason);
381 self._readdEntry(uidAuthor, oData);
382 self._oDb.maybeCommit(fCommit);
383 return True;
384
385
386 def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False):
387 """
388 Deletes a failure reason.
389 """
390 _ = fCascade; # too complicated for now.
391
392 #
393 # Check whether it's being used by other tables and bitch if it is .
394 # We currently do not implement cascading.
395 #
396 self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n'
397 'FROM BuildBlacklist\n'
398 'WHERE idFailureReason = %s\n'
399 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
400 'UNION\n'
401 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n'
402 'FROM TestResultFailures\n'
403 'WHERE idFailureReason = %s\n'
404 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
405 , (idFailureReason, idFailureReason,));
406 aaoRows = self._oDb.fetchAll();
407 if aaoRows:
408 raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s'
409 % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),));
410
411 #
412 # Do the job.
413 #
414 oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason);
415 assert oData.idFailureReason == idFailureReason;
416 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
417 if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
418 self._historizeEntry(idFailureReason, tsCurMinusOne);
419 self._readdEntry(uidAuthor, oData, tsCurMinusOne);
420 self._historizeEntry(idFailureReason);
421 self._oDb.maybeCommit(fCommit);
422 return True;
423
424
425 def cachedLookup(self, idFailureReason):
426 """
427 Looks up the most recent FailureReasonDataEx object for idFailureReason
428 via an object cache.
429
430 Returns a shared FailureReasonData object. None if not found.
431 Raises exception on DB error.
432 """
433 if self.dCache is None:
434 self.dCache = self._oDb.getCache('FailureReasonDataEx');
435 oEntry = self.dCache.get(idFailureReason, None);
436 if oEntry is None:
437 self._oDb.execute('SELECT *\n'
438 'FROM FailureReasons\n'
439 'WHERE idFailureReason = %s\n'
440 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
441 , (idFailureReason, ));
442 if self._oDb.getRowCount() == 0:
443 # Maybe it was deleted, try get the last entry.
444 self._oDb.execute('SELECT *\n'
445 'FROM FailureReasons\n'
446 'WHERE idFailureReason = %s\n'
447 'ORDER BY tsExpire DESC\n'
448 'LIMIT 1\n'
449 , (idFailureReason, ));
450 elif self._oDb.getRowCount() > 1:
451 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
452
453 if self._oDb.getRowCount() == 1:
454 self._ensureCachesPresent();
455 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
456 self.oUserAccountLogic);
457 self.dCache[idFailureReason] = oEntry;
458 return oEntry;
459
460
461 def cachedLookupByNameAndCategory(self, sName, sCategory):
462 """
463 Looks up a failure reason by it's name and category.
464
465 Should the request be ambigiuos, we'll return the oldest one.
466
467 Returns a shared FailureReasonData object. None if not found.
468 Raises exception on DB error.
469 """
470 if self.dCacheNameAndCat is None:
471 self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
472 sKey = '%s:::%s' % (sName, sCategory,);
473 oEntry = self.dCacheNameAndCat.get(sKey, None);
474 if oEntry is None:
475 self._oDb.execute('SELECT *\n'
476 'FROM FailureReasons,\n'
477 ' FailureCategories\n'
478 'WHERE FailureReasons.sShort = %s\n'
479 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
480 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory '
481 ' AND FailureCategories.sShort = %s\n'
482 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
483 'ORDER BY FailureReasons.tsEffective\n'
484 , ( sName, sCategory));
485 if self._oDb.getRowCount() == 0:
486 sLikeSucks = self._oDb.formatBindArgs(
487 'SELECT *\n'
488 'FROM FailureReasons,\n'
489 ' FailureCategories\n'
490 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n'
491 ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n'
492 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
493 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
494 ' AND ( FailureCategories.sShort = %s\n'
495 ' OR FailureCategories.sFull = %s)\n'
496 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
497 'ORDER BY FailureReasons.tsEffective\n'
498 , ( sName, sName, sCategory, sCategory ));
499 sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\'');
500 self._oDb.execute(sLikeSucks);
501 if self._oDb.getRowCount() > 0:
502 self._ensureCachesPresent();
503 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
504 self.oUserAccountLogic);
505 self.dCacheNameAndCat[sKey] = oEntry;
506 if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
507 sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
508 self.dCacheNameAndCat[sKey2] = oEntry;
509 return oEntry;
510
511
512 #
513 # Helpers.
514 #
515
516 def _readdEntry(self, uidAuthor, oData, tsEffective = None):
517 """
518 Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry.
519 """
520 if tsEffective is None:
521 tsEffective = self._oDb.getCurrentTimestamp();
522 self._oDb.execute('INSERT INTO FailureReasons (\n'
523 ' uidAuthor,\n'
524 ' tsEffective,\n'
525 ' idFailureReason,\n'
526 ' idFailureCategory,\n'
527 ' sShort,\n'
528 ' sFull,\n'
529 ' iTicket,\n'
530 ' asUrls)\n'
531 'VALUES (%s, %s, '
532 + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) )
533 + ', %s, %s, %s, %s, %s)\n'
534 , ( uidAuthor,
535 tsEffective,
536 oData.idFailureCategory,
537 oData.sShort,
538 oData.sFull,
539 oData.iTicket,
540 oData.asUrls,) );
541 return True;
542
543
544 def _historizeEntry(self, idFailureReason, tsExpire = None):
545 """ Historizes the current entry. """
546 if tsExpire is None:
547 tsExpire = self._oDb.getCurrentTimestamp();
548 self._oDb.execute('UPDATE FailureReasons\n'
549 'SET tsExpire = %s\n'
550 'WHERE idFailureReason = %s\n'
551 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
552 , (tsExpire, idFailureReason,));
553 return True;
554
555
556 def _ensureCachesPresent(self):
557 """ Ensures we've got the cache references resolved. """
558 if self.oCategoryLogic is None:
559 from testmanager.core.failurecategory import FailureCategoryLogic;
560 self.oCategoryLogic = FailureCategoryLogic(self._oDb);
561 if self.oUserAccountLogic is None:
562 self.oUserAccountLogic = UserAccountLogic(self._oDb);
563 return True;
564
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette