CMS 3D CMS Logo

uploadConditions.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 '''Script that uploads to the new CMS conditions uploader.
3 Adapted to the new infrastructure from v6 of the upload.py script for the DropBox from Miguel Ojeda.
4 '''
5 from __future__ import print_function
6 
7 __author__ = 'Andreas Pfeiffer'
8 __copyright__ = 'Copyright 2015, CERN CMS'
9 __credits__ = ['Giacomo Govi', 'Salvatore Di Guida', 'Miguel Ojeda', 'Andreas Pfeiffer']
10 __license__ = 'Unknown'
11 __maintainer__ = 'Giacomo Govi'
12 __email__ = 'giacomo.govi@cern.ch'
13 __version__ = 1
14 
15 
16 import os
17 import sys
18 import optparse
19 import hashlib
20 import tarfile
21 import netrc
22 import getpass
23 import errno
24 import sqlite3
25 import cx_Oracle
26 import json
27 import tempfile
28 from datetime import datetime
29 
30 defaultBackend = 'online'
31 defaultHostname = 'cms-conddb-prod.cern.ch'
32 defaultDevHostname = 'cms-conddb-dev.cern.ch'
33 defaultUrlTemplate = 'https://%s/cmsDbUpload/'
34 defaultTemporaryFile = 'upload.tar.bz2'
35 defaultNetrcHost = 'ConditionUploader'
36 defaultWorkflow = 'offline'
37 prodLogDbSrv = 'cms_orcoff_prod'
38 devLogDbSrv = 'cms_orcoff_prep'
39 logDbSchema = 'CMS_COND_DROPBOX'
40 authPathEnvVar = 'COND_AUTH_PATH'
41 waitForRetry = 15
42 
43 # common/http.py start (plus the "# Try to extract..." section bit)
44 import time
45 import logging
46 import io
47 
48 import pycurl
49 import socket
50 import copy
51 
52 def getInput(default, prompt = ''):
53  '''Like input() but with a default and automatic strip().
54  '''
55 
56  answer = input(prompt)
57  if answer:
58  return answer.strip()
59 
60  return default.strip()
61 
62 
63 def getInputWorkflow(prompt = ''):
64  '''Like getInput() but tailored to get target workflows (synchronization options).
65  '''
66 
67  while True:
68  workflow = getInput(defaultWorkflow, prompt)
69 
70  if workflow in frozenset(['offline', 'hlt', 'express', 'prompt', 'pcl']):
71  return workflow
72 
73  logging.error('Please specify one of the allowed workflows. See above for the explanation on each of them.')
74 
75 
76 def getInputChoose(optionsList, default, prompt = ''):
77  '''Makes the user choose from a list of options.
78  '''
79 
80  while True:
81  index = getInput(default, prompt)
82 
83  try:
84  return optionsList[int(index)]
85  except ValueError:
86  logging.error('Please specify an index of the list (i.e. integer).')
87  except IndexError:
88  logging.error('The index you provided is not in the given list.')
89 
90 
91 def getInputRepeat(prompt = ''):
92  '''Like input() but repeats if nothing is provided and automatic strip().
93  '''
94 
95  while True:
96  answer = input(prompt)
97  if answer:
98  return answer.strip()
99 
100  logging.error('You need to provide a value.')
101 
102 
103 def runWizard(basename, dataFilename, metadataFilename):
104  while True:
105  print('''\nWizard for metadata for %s
106 
107 I will ask you some questions to fill the metadata file. For some of the questions there are defaults between square brackets (i.e. []), leave empty (i.e. hit Enter) to use them.''' % basename)
108 
109  # Try to get the available inputTags
110  dataConnection = sqlite3.connect(dataFilename)
111  dataCursor = dataConnection.cursor()
112 
113  dataCursor.execute('select NAME from TAG')
114  records = dataCursor.fetchall()
115  inputTags = []
116  for rec in records:
117  inputTags.append(rec[0])
118 
119  if len(inputTags) == 0:
120  raise Exception("Could not find any input tag in the data file.")
121 
122  else:
123  print('\nI found the following input tags in your SQLite data file:')
124  for (index, inputTag) in enumerate(inputTags):
125  print(' %s) %s' % (index, inputTag))
126 
127  inputTag = getInputChoose(inputTags, '0',
128  '\nWhich is the input tag (i.e. the tag to be read from the SQLite data file)?\ne.g. 0 (you select the first in the list)\ninputTag [0]: ')
129 
130  destinationDatabase = ''
131  ntry = 0
132  while ( destinationDatabase != 'oracle://cms_orcon_prod/CMS_CONDITIONS' and destinationDatabase != 'oracle://cms_orcoff_prep/CMS_CONDITIONS' ):
133  if ntry==0:
134  inputMessage = \
135  '\nWhich is the destination database where the tags should be exported? \nPossible choices: oracle://cms_orcon_prod/CMS_CONDITIONS (or prod); oracle://cms_orcoff_prep/CMS_CONDITIONS (or prep) \ndestinationDatabase: '
136  elif ntry==1:
137  inputMessage = \
138  '\nPlease choose one of the two valid destinations: \noracle://cms_orcon_prod/CMS_CONDITIONS (for prod) or oracle://cms_orcoff_prep/CMS_CONDITIONS (for prep) \
139 \ndestinationDatabase: '
140  else:
141  raise Exception('No valid destination chosen. Bailing out...')
142  destinationDatabase = getInputRepeat(inputMessage)
143  if destinationDatabase == 'prod':
144  destinationDatabase = 'oracle://cms_orcon_prod/CMS_CONDITIONS'
145  if destinationDatabase == 'prep':
146  destinationDatabase = 'oracle://cms_orcoff_prep/CMS_CONDITIONS'
147  ntry += 1
148 
149  while True:
150  since = getInput('',
151  '\nWhich is the given since? (if not specified, the one from the SQLite data file will be taken -- note that even if specified, still this may not be the final since, depending on the synchronization options you select later: if the synchronization target is not offline, and the since you give is smaller than the next possible one (i.e. you give a run number earlier than the one which will be started/processed next in prompt/hlt/express), the DropBox will move the since ahead to go to the first safe run instead of the value you gave)\ne.g. 1234\nsince []: ')
152  if not since:
153  since = None
154  break
155  else:
156  try:
157  since = int(since)
158  break
159  except ValueError:
160  logging.error('The since value has to be an integer or empty (null).')
161 
162  userText = getInput('',
163  '\nWrite any comments/text you may want to describe your request\ne.g. Muon alignment scenario for...\nuserText []: ')
164 
165  destinationTags = {}
166  while True:
167  destinationTag = getInput('',
168  '\nWhich is the next destination tag to be added (leave empty to stop)?\ne.g. BeamSpotObjects_PCL_byRun_v0_offline\ndestinationTag []: ')
169  if not destinationTag:
170  if len(destinationTags) == 0:
171  logging.error('There must be at least one destination tag.')
172  continue
173  break
174 
175  if destinationTag in destinationTags:
176  logging.warning(
177  'You already added this destination tag. Overwriting the previous one with this new one.')
178 
179  destinationTags[destinationTag] = {
180  }
181 
182  metadata = {
183  'destinationDatabase': destinationDatabase,
184  'destinationTags': destinationTags,
185  'inputTag': inputTag,
186  'since': since,
187  'userText': userText,
188  }
189 
190  metadata = json.dumps(metadata, sort_keys=True, indent=4)
191  print('\nThis is the generated metadata:\n%s' % metadata)
192 
193  if getInput('n',
194  '\nIs it fine (i.e. save in %s and *upload* the conditions if this is the latest file)?\nAnswer [n]: ' % metadataFilename).lower() == 'y':
195  break
196  logging.info('Saving generated metadata in %s...', metadataFilename)
197  with open(metadataFilename, 'w') as metadataFile:
198  metadataFile.write(metadata)
199 
201  '''A common HTTP exception.
202 
203  self.code is the response HTTP code as an integer.
204  self.response is the response body (i.e. page).
205  '''
206 
207  def __init__(self, code, response):
208  self.code = code
209  self.response = response
210 
211  # Try to extract the error message if possible (i.e. known error page format)
212  try:
213  self.args = (response.split('<p>')[1].split('</p>')[0], )
214  except Exception:
215  self.args = (self.response, )
216 
217 
218 CERN_SSO_CURL_CAPATH = '/etc/pki/tls/certs'
219 
220 class HTTP(object):
221  '''Class used for querying URLs using the HTTP protocol.
222  '''
223 
224  retryCodes = frozenset([502, 503])
225 
226  def __init__(self):
227  self.setBaseUrl()
228  self.setRetries()
229 
230  self.curl = pycurl.Curl()
231  self.curl.setopt(self.curl.COOKIEFILE, '') # in memory
232 
233  #-toDo: make sure we have the right options set here to use ssl
234  #-review(2015-09-25): check and see - action: AP
235  # self.curl.setopt(self.curl.SSL_VERIFYPEER, 1)
236  self.curl.setopt(self.curl.SSL_VERIFYPEER, 0)
237  self.curl.setopt(self.curl.SSL_VERIFYHOST, 2)
238 
239  self.baseUrl = None
240 
241  self.token = None
242 
243  def getCookies(self):
244  '''Returns the list of cookies.
245  '''
246  return self.curl.getinfo(self.curl.INFO_COOKIELIST)
247 
248  def discardCookies(self):
249  '''Discards cookies.
250  '''
251  self.curl.setopt(self.curl.COOKIELIST, 'ALL')
252 
253 
254  def setBaseUrl(self, baseUrl = ''):
255  '''Allows to set a base URL which will be prefixed to all the URLs
256  that will be queried later.
257  '''
258  self.baseUrl = baseUrl
259 
260 
261  def setProxy(self, proxy = ''):
262  '''Allows to set a proxy.
263  '''
264  self.curl.setopt(self.curl.PROXY, proxy)
265 
266 
267  def setTimeout(self, timeout = 0):
268  '''Allows to set a timeout.
269  '''
270  self.curl.setopt(self.curl.TIMEOUT, timeout)
271 
272 
273  def setRetries(self, retries = ()):
274  '''Allows to set retries.
275 
276  The retries are a sequence of the seconds to wait per retry.
277 
278  The retries are done on:
279  * PyCurl errors (includes network problems, e.g. not being able
280  to connect to the host).
281  * 502 Bad Gateway (for the moment, to avoid temporary
282  Apache-CherryPy issues).
283  * 503 Service Temporarily Unavailable (for when we update
284  the frontends).
285  '''
286  self.retries = retries
287 
288  def getToken(self, username, password):
289 
290  url = self.baseUrl + 'token'
291 
292  self.curl.setopt(pycurl.URL, url)
293  self.curl.setopt(pycurl.VERBOSE, 0)
294 
295  #-toDo: check if/why these are needed ...
296  #-ap: hmm ...
297  # self.curl.setopt(pycurl.DNS_CACHE_TIMEOUT, 0)
298  # self.curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
299  #-end hmmm ...
300  #-review(2015-09-25): check and see - action: AP
301 
302  self.curl.setopt(pycurl.HTTPHEADER, ['Accept: application/json'])
303  # self.curl.setopt( self.curl.POST, {})
304  self.curl.setopt(self.curl.HTTPGET, 0)
305 
306  response = io.BytesIO()
307  self.curl.setopt(pycurl.WRITEFUNCTION, response.write)
308  self.curl.setopt(pycurl.USERPWD, '%s:%s' % (username, password) )
309  logging.debug('going to connect to server at: %s' % url )
310 
311  self.curl.perform()
312  code = self.curl.getinfo(pycurl.RESPONSE_CODE)
313  logging.debug('got: %s ', str(code))
314  if code in ( 502,503,504 ):
315  logging.debug('Trying again after %d seconds...', waitForRetry)
316  time.sleep( waitForRetry )
317  response = io.StringIO()
318  self.curl.setopt(pycurl.WRITEFUNCTION, response.write)
319  self.curl.setopt(pycurl.USERPWD, '%s:%s' % (username, password) )
320  self.curl.perform()
321  code = self.curl.getinfo(pycurl.RESPONSE_CODE)
322  resp = response.getvalue().decode('UTF-8')
323  errorMsg = None
324  if code==500 and not resp.find("INVALID_CREDENTIALS")==-1:
325  logging.error("Invalid credentials provided.")
326  return None
327  if code==403 and not resp.find("Unauthorized access")==-1:
328  logging.error("Unauthorized access. Please check the membership of group 'cms-cond-dropbox'")
329  return None
330  if code==200:
331  try:
332  self.token = json.loads( resp )['token']
333  except Exception as e:
334  errorMsg = 'Error while decoding returned json string'
335  logging.debug('http::getToken> error while decoding json: %s ', str(resp) )
336  logging.debug("error getting token: %s", str(e))
337  resp = None
338  else:
339  errorMsg = 'HTTP Error code %s ' %code
340  logging.debug('got: %s ', str(code))
341  logging.debug('http::getToken> got error from server: %s ', str(resp) )
342  resp = None
343  if resp is None:
344  raise Exception(errorMsg)
345 
346  logging.debug('token: %s', self.token)
347  logging.debug('returning: %s', response.getvalue().decode('UTF-8'))
348 
349  return response.getvalue()
350 
351  def query(self, url, data = None, files = None, keepCookies = True):
352  '''Queries a URL, optionally with some data (dictionary).
353 
354  If no data is specified, a GET request will be used.
355  If some data is specified, a POST request will be used.
356 
357  If files is specified, it must be a dictionary like data but
358  the values are filenames.
359 
360  By default, cookies are kept in-between requests.
361 
362  A HTTPError exception is raised if the response's HTTP code is not 200.
363  '''
364 
365  if not keepCookies:
366  self.discardCookies()
367 
368  url = self.baseUrl + url
369 
370  # make sure the logs are safe ... at least somewhat :)
371  data4log = copy.copy(data)
372  if data4log:
373  if 'password' in data4log.keys():
374  data4log['password'] = '*'
375 
376  retries = [0] + list(self.retries)
377 
378  while True:
379  logging.debug('Querying %s with data %s and files %s (retries left: %s, current sleep: %s)...', url, data4log, files, len(retries), retries[0])
380 
381  time.sleep(retries.pop(0))
382 
383  try:
384  self.curl.setopt(self.curl.URL, url)
385  self.curl.setopt(self.curl.HTTPGET, 1)
386 
387  # from now on we use the token we got from the login
388  self.curl.setopt(pycurl.USERPWD, '%s:""' % ( str(self.token), ) )
389  self.curl.setopt(pycurl.HTTPHEADER, ['Accept: application/json'])
390 
391  if data is not None or files is not None:
392  # If there is data or files to send, use a POST request
393 
394  finalData = {}
395 
396  if data is not None:
397  finalData.update(data)
398 
399  if files is not None:
400  for (key, fileName) in files.items():
401  finalData[key] = (self.curl.FORM_FILE, fileName)
402  self.curl.setopt( self.curl.HTTPPOST, list(finalData.items()) )
403 
404  self.curl.setopt(pycurl.VERBOSE, 0)
405 
406  response = io.BytesIO()
407  self.curl.setopt(self.curl.WRITEFUNCTION, response.write)
408  self.curl.perform()
409 
410  code = self.curl.getinfo(self.curl.RESPONSE_CODE)
411 
412  if code in self.retryCodes and len(retries) > 0:
413  logging.debug('Retrying since we got the %s error code...', code)
414  continue
415 
416  if code != 200:
417  raise HTTPError(code, response.getvalue())
418 
419  return response.getvalue()
420 
421  except pycurl.error as e:
422  if len(retries) == 0:
423  raise e
424  logging.debug('Retrying since we got the %s pycurl exception...', str(e))
425 
426 # common/http.py end
427 
428 def addToTarFile(tarFile, fileobj, arcname):
429  tarInfo = tarFile.gettarinfo(fileobj = fileobj, arcname = arcname)
430  tarInfo.mode = 0o400
431  tarInfo.uid = tarInfo.gid = tarInfo.mtime = 0
432  tarInfo.uname = tarInfo.gname = 'root'
433  tarFile.addfile(tarInfo, fileobj)
434 
436  '''Upload conditions to the CMS conditions uploader service.
437  '''
438 
439  def __init__(self, hostname = defaultHostname, urlTemplate = defaultUrlTemplate):
440  self.hostname = hostname
441  self.urlTemplate = urlTemplate
442  self.userName = None
443  self.http = None
444  self.password = None
445  self.token = None
446 
447  def setHost( self, hostname ):
448  if not hostname==self.hostname:
449  self.token = None
450  self.hostname = hostname
451 
452  def signIn(self, username, password ):
453  if self.token is None:
454  logging.debug("Initializing connection with server %s",self.hostname)
455  ''' init the server.
456  '''
457  self.http = HTTP()
458  if socket.getfqdn().strip().endswith('.cms'):
459  self.http.setProxy('https://cmsproxy.cms:3128/')
460  self.http.setBaseUrl(self.urlTemplate % self.hostname)
461  '''Signs in the server.
462  '''
463  logging.info('%s: Signing in user %s ...', self.hostname, username)
464  try:
465  self.token = self.http.getToken(username, password)
466  except Exception as e:
467  ret = -1
468  # optionally, we may want to have a different return for network related errors:
469  #code = self.http.curl.getinfo(pycurl.RESPONSE_CODE)
470  #if code in ( 502,503,504 ):
471  # ret = -10
472  logging.error("Caught exception when trying to connect to %s: %s" % (self.hostname, str(e)) )
473  return ret
474 
475  if not self.token:
476  logging.error("could not get token for user %s from %s" % (username, self.hostname) )
477  return -2
478  logging.debug( "got: '%s'", str(self.token) )
479  self.userName = username
480  self.password = password
481  else:
482  logging.debug("User %s has been already authenticated." %username)
483  return 0
484 
485  def signOut(self):
486  '''Signs out the server.
487  '''
488 
489  logging.info('%s: Signing out...', self.hostname)
490  # self.http.query('logout')
491  self.token = None
492 
493 
494  def _checkForUpdates(self):
495  '''Updates this script, if a new version is found.
496  '''
497 
498  logging.debug('%s: Checking if a newer version of this script is available ...', self.hostname)
499  version = int(self.http.query('getUploadScriptVersion'))
500 
501  if version <= __version__:
502  logging.debug('%s: Script is up-to-date.', self.hostname)
503  return
504 
505  logging.info('%s: Updating to a newer version (%s) than the current one (%s): downloading ...', self.hostname, version, __version__)
506 
507  uploadScript = self.http.query('getUploadScript')
508 
509  self.signOut()
510 
511  logging.info('%s: ... saving the new version ...', self.hostname)
512  with open(sys.argv[0], 'wb') as f:
513  f.write(uploadScript)
514 
515  logging.info('%s: ... executing the new version...', self.hostname)
516  os.execl(sys.executable, *([sys.executable] + sys.argv))
517 
518 
519  def uploadFile(self, filename, backend = defaultBackend, temporaryFile = defaultTemporaryFile):
520  '''Uploads a file to the dropBox.
521 
522  The filename can be without extension, with .db or with .txt extension.
523  It will be stripped and then both .db and .txt files are used.
524  '''
525 
526  basepath = filename.rsplit('.db', 1)[0].rsplit('.txt', 1)[0]
527  basename = os.path.basename(basepath)
528 
529  logging.debug('%s: %s: Creating tar file for upload ...', self.hostname, basename)
530 
531  try:
532  tarFile = tarfile.open(temporaryFile, 'w:bz2')
533 
534  with open('%s.db' % basepath, 'rb') as data:
535  addToTarFile(tarFile, data, 'data.db')
536  except Exception as e:
537  msg = 'Error when creating tar file. \n'
538  msg += 'Please check that you have write access to the directory you are running,\n'
539  msg += 'and that you have enough space on this disk (df -h .)\n'
540  logging.error(msg)
541  raise Exception(msg)
542 
543  with tempfile.NamedTemporaryFile(mode='rb+') as metadata:
544  with open('%s.txt' % basepath, 'r') as originalMetadata:
545  metadata.write(json.dumps(json.load(originalMetadata), sort_keys = True, indent = 4).encode())
546 
547  metadata.seek(0)
548  addToTarFile(tarFile, metadata, 'metadata.txt')
549 
550  tarFile.close()
551 
552  logging.debug('%s: %s: Calculating hash...', self.hostname, basename)
553 
554  fileHash = hashlib.sha1()
555  with open(temporaryFile, 'rb') as f:
556  while True:
557  data = f.read(4 * 1024 * 1024)
558  if not data:
559  break
560  fileHash.update(data)
561 
562  fileHash = fileHash.hexdigest()
563  fileInfo = os.stat(temporaryFile)
564  fileSize = fileInfo.st_size
565 
566  logging.debug('%s: %s: Hash: %s', self.hostname, basename, fileHash)
567 
568  logging.info('%s: %s: Uploading file (%s, size %s) to the %s backend...', self.hostname, basename, fileHash, fileSize, backend)
569  os.rename(temporaryFile, fileHash)
570  try:
571  ret = self.http.query('uploadFile',
572  {
573  'backend': backend,
574  'fileName': basename,
575  'userName': self.userName,
576  },
577  files = {
578  'uploadedFile': fileHash,
579  }
580  )
581  except Exception as e:
582  logging.error('Error from uploading: %s' % str(e))
583  ret = json.dumps( { "status": -1, "upload" : { 'itemStatus' : { basename : {'status':'failed', 'info':str(e)}}}, "error" : str(e)} )
584 
585  os.unlink(fileHash)
586 
587  statusInfo = json.loads(ret)['upload']
588  logging.debug( 'upload returned: %s', statusInfo )
589 
590  okTags = []
591  skippedTags = []
592  failedTags = []
593  for tag, info in statusInfo['itemStatus'].items():
594  logging.debug('checking tag %s, info %s', tag, str(json.dumps(info, indent=4,sort_keys=True)) )
595  if 'ok' in info['status'].lower() :
596  okTags.append( tag )
597  logging.info('tag %s successfully uploaded', tag)
598  if 'skip' in info['status'].lower() :
599  skippedTags.append( tag )
600  logging.warning('found tag %s to be skipped. reason: \n ... \t%s ', tag, info['info'])
601  if 'fail' in info['status'].lower() :
602  failedTags.append( tag )
603  logging.error('found tag %s failed to upload. reason: \n ... \t%s ', tag, info['info'])
604 
605  if len(okTags) > 0: logging.info ("tags sucessfully uploaded: %s ", str(okTags) )
606  if len(skippedTags) > 0: logging.warning("tags SKIPped to upload : %s ", str(skippedTags) )
607  if len(failedTags) > 0: logging.error ("tags FAILed to upload : %s ", str(failedTags) )
608 
609  fileLogURL = 'https://cms-conddb.cern.ch/cmsDbBrowser/logs/show_cond_uploader_log/%s/%s'
610  backend = 'Prod'
611  if self.hostname=='cms-conddb-dev.cern.ch':
612  backend = 'Prep'
613  logging.info('file log at: %s', fileLogURL % (backend,fileHash))
614 
615  return len(okTags)>0
616 
617 def getCredentials( options ):
618 
619  username = None
620  password = None
621  netrcPath = None
622  if authPathEnvVar in os.environ:
623  authPath = os.environ[authPathEnvVar]
624  netrcPath = os.path.join(authPath,'.netrc')
625  if options.authPath is not None:
626  netrcPath = os.path.join( options.authPath,'.netrc' )
627  try:
628  # Try to find the netrc entry
629  (username, account, password) = netrc.netrc( netrcPath ).authenticators(options.netrcHost)
630  except Exception:
631  # netrc entry not found, ask for the username and password
632  logging.info(
633  'netrc entry "%s" not found: if you wish not to have to retype your password, you can add an entry in your .netrc file. However, beware of the risks of having your password stored as plaintext. Instead.',
634  options.netrcHost)
635 
636  # Try to get a default username
637  defaultUsername = getpass.getuser()
638  if defaultUsername is None:
639  defaultUsername = '(not found)'
640 
641  username = getInput(defaultUsername, '\nUsername [%s]: ' % defaultUsername)
642  password = getpass.getpass('Password: ')
643 
644  return username, password
645 
646 
647 def uploadAllFiles(options, arguments):
648 
649  ret = {}
650  ret['status'] = 0
651 
652  # Check that we can read the data and metadata files
653  # If the metadata file does not exist, start the wizard
654  for filename in arguments:
655  basepath = filename.rsplit('.db', 1)[0].rsplit('.txt', 1)[0]
656  basename = os.path.basename(basepath)
657  dataFilename = '%s.db' % basepath
658  metadataFilename = '%s.txt' % basepath
659 
660  logging.info('Checking %s...', basename)
661 
662  # Data file
663  try:
664  with open(dataFilename, 'rb') as dataFile:
665  pass
666  except IOError as e:
667  errMsg = 'Impossible to open SQLite data file %s' %dataFilename
668  logging.error( errMsg )
669  ret['status'] = -3
670  ret['error'] = errMsg
671  return ret
672 
673  # Check the data file
674  empty = True
675  try:
676  dbcon = sqlite3.connect( dataFilename )
677  dbcur = dbcon.cursor()
678  dbcur.execute('SELECT * FROM IOV')
679  rows = dbcur.fetchall()
680  for r in rows:
681  empty = False
682  dbcon.close()
683  if empty:
684  errMsg = 'The input SQLite data file %s contains no data.' %dataFilename
685  logging.error( errMsg )
686  ret['status'] = -4
687  ret['error'] = errMsg
688  return ret
689  except Exception as e:
690  errMsg = 'Check on input SQLite data file %s failed: %s' %(dataFilename,str(e))
691  logging.error( errMsg )
692  ret['status'] = -5
693  ret['error'] = errMsg
694  return ret
695 
696  # Metadata file
697  try:
698  with open(metadataFilename, 'rb') as metadataFile:
699  pass
700  except IOError as e:
701  if e.errno != errno.ENOENT:
702  errMsg = 'Impossible to open file %s (for other reason than not existing)' %metadataFilename
703  logging.error( errMsg )
704  ret['status'] = -4
705  ret['error'] = errMsg
706  return ret
707 
708  if getInput('y', '\nIt looks like the metadata file %s does not exist. Do you want me to create it and help you fill it?\nAnswer [y]: ' % metadataFilename).lower() != 'y':
709  errMsg = 'Metadata file %s does not exist' %metadataFilename
710  logging.error( errMsg )
711  ret['status'] = -5
712  ret['error'] = errMsg
713  return ret
714  # Wizard
715  runWizard(basename, dataFilename, metadataFilename)
716 
717  # Upload files
718  try:
719  dropBox = ConditionsUploader(options.hostname, options.urlTemplate)
720 
721  # Authentication
722  username, password = getCredentials(options)
723 
724  results = {}
725  for filename in arguments:
726  backend = options.backend
727  basepath = filename.rsplit('.db', 1)[0].rsplit('.txt', 1)[0]
728  metadataFilename = '%s.txt' % basepath
729  with open(metadataFilename, 'rb') as metadataFile:
730  metadata = json.load( metadataFile )
731  # When dest db = prep the hostname has to be set to dev.
732  forceHost = False
733  destDb = metadata['destinationDatabase']
734  if destDb.startswith('oracle://cms_orcon_prod') or destDb.startswith('oracle://cms_orcoff_prep'):
735  hostName = defaultHostname
736  if destDb.startswith('oracle://cms_orcoff_prep'):
737  hostName = defaultDevHostname
738  dropBox.setHost( hostName )
739  authRet = dropBox.signIn( username, password )
740  if not authRet==0:
741  msg = "Error trying to connect to the server. Aborting."
742  if authRet==-2:
743  msg = "Error while signin in. Aborting."
744  logging.error(msg)
745  return { 'status' : authRet, 'error' : msg }
746  results[filename] = dropBox.uploadFile(filename, options.backend, options.temporaryFile)
747  else:
748  results[filename] = False
749  logging.error("DestinationDatabase %s is not valid. Skipping the upload." %destDb)
750  if not results[filename]:
751  if ret['status']<0:
752  ret['status'] = 0
753  ret['status'] += 1
754  ret['files'] = results
755  logging.debug("all files processed, logging out now.")
756 
757  dropBox.signOut()
758 
759  except HTTPError as e:
760  logging.error('got HTTP error: %s', str(e))
761  return { 'status' : -1, 'error' : str(e) }
762 
763  return ret
764 
765 def uploadTier0Files(filenames, username, password, cookieFileName = None):
766  '''Uploads a bunch of files coming from Tier0.
767  This has the following requirements:
768  * Username/Password based authentication.
769  * Uses the online backend.
770  * Ignores errors related to the upload/content (e.g. duplicated file).
771  '''
772 
773  dropBox = ConditionsUploader()
774 
775  dropBox.signIn(username, password)
776 
777  for filename in filenames:
778  try:
779  result = dropBox.uploadFile(filename, backend = 'test')
780  except HTTPError as e:
781  if e.code == 400:
782  # 400 Bad Request: This is an exception related to the upload
783  # being wrong for some reason (e.g. duplicated file).
784  # Since for Tier0 this is not an issue, continue
785  logging.error('HTTP Exception 400 Bad Request: Upload-related, skipping. Message: %s', e)
786  continue
787 
788  # In any other case, re-raise.
789  raise
790 
791  #-toDo: add a flag to say if we should retry or not. So far, all retries are done server-side (Tier-0),
792  # if we flag as failed any retry would not help and would result in the same error (e.g.
793  # when a file with an identical hash is uploaded again)
794  #-review(2015-09-25): get feedback from tests at Tier-0 (action: AP)
795 
796  if not result: # dropbox reported an error when uploading, do not retry.
797  logging.error('Error from dropbox, upload-related, skipping.')
798  continue
799 
800  dropBox.signOut()
801 
802 def re_upload( options ):
803  netrcPath = None
804  logDbSrv = prodLogDbSrv
805  if options.hostname == defaultDevHostname:
806  logDbSrv = devLogDbSrv
807  if options.authPath is not None:
808  netrcPath = os.path.join( options.authPath,'.netrc' )
809  try:
810  netrcKey = '%s/%s' %(logDbSrv,logDbSchema)
811  # Try to find the netrc entry
812  (username, account, password) = netrc.netrc( netrcPath ).authenticators( netrcKey )
813  except IOError as e:
814  logging.error('Cannot access netrc file.')
815  return 1
816  except Exception as e:
817  logging.error('Netrc file is invalid: %s' %str(e))
818  return 1
819  conStr = '%s/%s@%s' %(username,password,logDbSrv)
820  con = cx_Oracle.connect( conStr )
821  cur = con.cursor()
822  fh = options.reUpload
823  cur.execute('SELECT FILECONTENT, STATE FROM FILES WHERE FILEHASH = :HASH',{'HASH':fh})
824  res = cur.fetchall()
825  found = False
826  fdata = None
827  for r in res:
828  found = True
829  logging.info("Found file %s in state '%s;" %(fh,r[1]))
830  fdata = r[0].read().decode('bz2')
831  con.close()
832  if not found:
833  logging.error("No file uploaded found with hash %s" %fh)
834  return 1
835  # writing as a tar file and open it ( is there a why to open it in memory?)
836  fname = '%s.tar' %fh
837  with open(fname, "wb" ) as f:
838  f.write(fdata)
839  rname = 'reupload_%s' %fh
840  with tarfile.open(fname) as tar:
841  tar.extractall()
842  os.remove(fname)
843  dfile = 'data.db'
844  mdfile = 'metadata.txt'
845  if os.path.exists(dfile):
846  os.utime(dfile,None)
847  os.chmod(dfile,0o755)
848  os.rename(dfile,'%s.db' %rname)
849  else:
850  logging.error('Tar file does not contain the data file')
851  return 1
852  if os.path.exists(mdfile):
853  os.utime(mdfile,None)
854  os.chmod(mdfile,0o755)
855  mdata = None
856  with open(mdfile) as md:
857  mdata = json.load(md)
858  datelabel = datetime.now().strftime("%y-%m-%d %H:%M:%S")
859  if mdata is None:
860  logging.error('Metadata file is empty.')
861  return 1
862  logging.debug('Preparing new metadata file...')
863  mdata['userText'] = 'reupload %s : %s' %(datelabel,mdata['userText'])
864  with open( '%s.txt' %rname, 'wb') as jf:
865  jf.write( json.dumps( mdata, sort_keys=True, indent = 2 ) )
866  jf.write('\n')
867  os.remove(mdfile)
868  else:
869  logging.error('Tar file does not contain the metadata file')
870  return 1
871  logging.info('Files %s prepared for the upload.' %rname)
872  arguments = [rname]
873  return upload(options, arguments)
874 
875 def upload(options, arguments):
876  results = uploadAllFiles(options, arguments)
877 
878  if 'status' not in results:
879  print('Unexpected error.')
880  return -1
881  ret = results['status']
882  print(results)
883  print("upload ended with code: %s" %ret)
884  return ret
885 
886 def main():
887  '''Entry point.
888  '''
889 
890  parser = optparse.OptionParser(usage =
891  'Usage: %prog [options] <file> [<file> ...]\n'
892  )
893 
894  parser.add_option('-d', '--debug',
895  dest = 'debug',
896  action="store_true",
897  default = False,
898  help = 'Switch on printing debug information. Default: %default',
899  )
900 
901  parser.add_option('-b', '--backend',
902  dest = 'backend',
903  default = defaultBackend,
904  help = 'dropBox\'s backend to upload to. Default: %default',
905  )
906 
907  parser.add_option('-H', '--hostname',
908  dest = 'hostname',
909  default = defaultHostname,
910  help = 'dropBox\'s hostname. Default: %default',
911  )
912 
913  parser.add_option('-u', '--urlTemplate',
914  dest = 'urlTemplate',
915  default = defaultUrlTemplate,
916  help = 'dropBox\'s URL template. Default: %default',
917  )
918 
919  parser.add_option('-f', '--temporaryFile',
920  dest = 'temporaryFile',
921  default = defaultTemporaryFile,
922  help = 'Temporary file that will be used to store the first tar file. Note that it then will be moved to a file with the hash of the file as its name, so there will be two temporary files created in fact. Default: %default',
923  )
924 
925  parser.add_option('-n', '--netrcHost',
926  dest = 'netrcHost',
927  default = defaultNetrcHost,
928  help = 'The netrc host (machine) from where the username and password will be read. Default: %default',
929  )
930 
931  parser.add_option('-a', '--authPath',
932  dest = 'authPath',
933  default = None,
934  help = 'The path of the .netrc file for the authentication. Default: $HOME',
935  )
936 
937  parser.add_option('-r', '--reUpload',
938  dest = 'reUpload',
939  default = None,
940  help = 'The hash of the file to upload again.',
941  )
942 
943  (options, arguments) = parser.parse_args()
944 
945  logLevel = logging.INFO
946  if options.debug:
947  logLevel = logging.DEBUG
948  logging.basicConfig(
949  format = '[%(asctime)s] %(levelname)s: %(message)s',
950  level = logLevel,
951  )
952 
953  if len(arguments) < 1:
954  if options.reUpload is None:
955  parser.print_help()
956  return -2
957  else:
958  return re_upload(options)
959  if options.reUpload is not None:
960  print("ERROR: options -r can't be specified on a new file upload.")
961  return -2
962 
963  return upload(options, arguments)
964 
966 
967  global defaultNetrcHost
968 
969  (username, account, password) = netrc.netrc().authenticators(defaultNetrcHost)
970 
971  filenames = ['testFiles/localSqlite-top2']
972 
973  uploadTier0Files(filenames, username, password, cookieFileName = None)
974 
975 
976 if __name__ == '__main__':
977 
978  sys.exit(main())
979  # testTier0Upload()
def setRetries(self, retries=())
def __init__(self, hostname=defaultHostname, urlTemplate=defaultUrlTemplate)
def uploadFile(self, filename, backend=defaultBackend, temporaryFile=defaultTemporaryFile)
def getInput(default, prompt='')
def setBaseUrl(self, baseUrl='')
def getToken(self, username, password)
def addToTarFile(tarFile, fileobj, arcname)
def query(self, url, data=None, files=None, keepCookies=True)
def getInputChoose(optionsList, default, prompt='')
def setProxy(self, proxy='')
def getCredentials(options)
def upload(options, arguments)
static std::string const input
Definition: EdmProvDump.cc:50
Definition: query.py:1
def setTimeout(self, timeout=0)
def uploadTier0Files(filenames, username, password, cookieFileName=None)
def __init__(self, code, response)
void print(TMatrixD &m, const char *label=nullptr, bool mathematicaFormat=false)
Definition: Utilities.cc:47
bool decode(bool &, std::string const &)
Definition: types.cc:71
def signIn(self, username, password)
def uploadAllFiles(options, arguments)
def re_upload(options)
def getInputWorkflow(prompt='')
def encode(args, files)
def runWizard(basename, dataFilename, metadataFilename)
Definition: main.py:1
#define str(s)
def getToken(db, tag, since)
def getInputRepeat(prompt='')