Script bodhi_client_py
[hide private]
[frames] | no frames]

Source Code for Script script-bodhi_client_py

  1  #!/usr/bin/python -tt 
  2  # $Id: $ 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU General Public License as published by 
  6  # the Free Software Foundation; version 2 of the License. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU Library General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU General Public License 
 14  # along with this program; if not, write to the Free Software 
 15  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 
 16  # 
 17  # Authors: Luke Macken <lmacken@redhat.com> 
 18   
 19  import re 
 20  import sys 
 21  import os 
 22  import json 
 23  import Cookie 
 24  import urllib 
 25  import urllib2 
 26  import getpass 
 27  import logging 
 28  import cPickle as pickle 
 29   
 30  from os.path import expanduser, join, isfile 
 31  from optparse import OptionParser 
 32   
 33  log = logging.getLogger(__name__) 
 34   
 35  __version__ = '$Revision: $'[11:-2] 
 36  __description__ = 'Command line tool for interacting with Bodhi' 
 37   
 38  BODHI_URL = 'http://localhost:8084/updates/' 
 39  SESSION_FILE = join(expanduser('~'), '.bodhi_session') 
 40   
41 -class AuthError(Exception):
42 pass
43
44 -class BodhiClient:
45 """ 46 A command-line client to interact with Bodhi. 47 """ 48 49 session = None 50
51 - def __init__(self, opts):
52 self.load_session() 53 54 if opts.new: 55 self.new(opts) 56 elif opts.testing: 57 self.push_to_testing(opts) 58 elif opts.stable: 59 self.push_to_stable(opts) 60 elif opts.masher: 61 self.masher(opts) 62 elif opts.push: 63 self.push(opts) 64 elif opts.delete: 65 self.delete(opts) 66 elif opts.status or opts.bugs or opts.cves or opts.release or opts.type: 67 self.list(opts)
68
69 - def authenticate(self):
70 """ 71 Return an authenticated session cookie. 72 """ 73 if self.session: 74 return self.session 75 76 sys.stdout.write("Username: ") 77 sys.stdout.flush() 78 username = sys.stdin.readline().strip() 79 password = getpass.getpass() 80 81 req = urllib2.Request(BODHI_URL + 'login?tg_format=json') 82 req.add_data(urllib.urlencode({ 83 'user_name' : username, 84 'password' : password, 85 'login' : 'Login' 86 })) 87 88 try: 89 f = urllib2.urlopen(req) 90 except urllib2.HTTPError, e: 91 if e.msg == "Forbidden": 92 raise AuthError, "Invalid username/password" 93 94 data = json.read(f.read()) 95 if 'message' in data: 96 raise AuthError, 'Unable to login to server: %s' % data['message'] 97 98 self.session = Cookie.SimpleCookie() 99 try: 100 self.session.load(f.headers['set-cookie']) 101 except KeyError: 102 raise AuthError, "Unable to login to the server. Server did not" \ 103 "send back a cookie." 104 self.save_session() 105 106 return self.session
107
108 - def save_session(self):
109 """ 110 Store a pickled session cookie. 111 """ 112 s = file(SESSION_FILE, 'w') 113 pickle.dump(self.session, s) 114 s.close()
115
116 - def load_session(self):
117 """ 118 Load a stored session cookie. 119 """ 120 if isfile(SESSION_FILE): 121 s = file(SESSION_FILE, 'r') 122 try: 123 self.session = pickle.load(s) 124 log.debug("Loaded session %s" % self.session) 125 except EOFError: 126 log.error("Unable to load session from %s" % SESSION_FILE) 127 s.close()
128
129 - def send_request(self, method, auth=False, **kw):
130 """ 131 Send a request to the server. The given method is called with any 132 keyword parameters in **kw. If auth is True, then the request is 133 made with an authenticated session cookie. 134 """ 135 url = BODHI_URL + method + "/?tg_format=json" 136 137 response = None # the JSON that we get back from bodhi 138 data = None # decoded JSON via json.read() 139 140 log.debug("Creating request %s" % url) 141 req = urllib2.Request(url) 142 req.add_data(urllib.urlencode(kw)) 143 144 if auth: 145 cookie = self.authenticate() 146 req.add_header('Cookie', cookie.output(attrs=[], 147 header='').strip()) 148 try: 149 response = urllib2.urlopen(req) 150 data = json.read(response.read()) 151 except urllib2.HTTPError, e: 152 log.error(e) 153 sys.exit(-1) 154 except urllib2.URLError, e: 155 log.error("No connection to Bodhi server") 156 log.error(e) 157 sys.exit(-1) 158 except json.ReadException, e: 159 regex = re.compile('<span class="fielderror">(.*)</span>') 160 match = regex.search(e.message) 161 if match and len(match.groups()): 162 log.error(match.groups()[0]) 163 else: 164 log.error("Unexpected ReadException during request:" + e) 165 sys.exit(-1) 166 167 return data
168
169 - def new(self, opts):
170 if opts.input_file: 171 self._parse_file(opts) 172 log.info("Creating new update for %s" % opts.new) 173 data = self.send_request('save', builds=opts.new, release=opts.release, 174 type=opts.type, bugs=opts.bugs, cves=opts.cves, 175 notes=opts.notes, auth=True) 176 log.info(data['tg_flash']) 177 if data.has_key('update'): 178 log.info(data['update'])
179
180 - def list(self, opts):
181 args = { 'tg_paginate_limit' : opts.limit } 182 for arg in ('release', 'status', 'type', 'bugs', 'cves'): 183 if getattr(opts, arg): 184 args[arg] = getattr(opts, arg) 185 data = self.send_request('list', **args) 186 if data.has_key('tg_flash') and data['tg_flash']: 187 log.error(data['tg_flash']) 188 sys.exit(-1) 189 for update in data['updates']: 190 log.info(update + '\n') 191 log.info("%d updates found (%d shown)" % (data['num_items'],opts.limit))
192
193 - def delete(self, opts):
194 data = self.send_request('delete', update=opts.delete, auth=True) 195 log.info(data['tg_flash'])
196
197 - def push_to_testing(self, opts):
198 data = self.send_request('push', nvr=opts.testing, auth=True) 199 log.info(data['tg_flash']) 200 if data.has_key('update'): 201 log.info(data['update'])
202
203 - def push_to_stable(self, opts):
204 data = self.send_request('move', nvr=opts.stable, auth=True) 205 log.info(data['tg_flash'])
206 207
208 - def masher(self, opts):
209 data = self.send_request('admin/masher', auth=True) 210 log.info(data['masher_str'])
211
212 - def push(self, opts):
213 data = self.send_request('admin/push', auth=True) 214 log.info("[ %d Pending Requests ]" % len(data['updates'])) 215 stable = filter(lambda x: x['request'] == 'stable', data['updates']) 216 testing = filter(lambda x: x['request'] == 'testing', data['updates']) 217 obsolete = filter(lambda x: x['request'] == 'obsolete', data['updates']) 218 for title, updates in (('Testing', testing), 219 ('Stable', stable), 220 ('Obsolete', obsolete)): 221 if len(updates): 222 log.info("\n" + title + "\n========") 223 for update in updates: 224 log.info("%s" % update['title']) 225 226 ## Confirm that we actually want to push these updates 227 sys.stdout.write("\nAre you sure you want to push these updates? ") 228 sys.stdout.flush() 229 yes = sys.stdin.readline().strip() 230 if yes in ('y', 'yes'): 231 log.info("Pushing!") 232 self.send_request('admin/push/mash', 233 updates=[u['title'] for u in data['updates']], 234 auth=True)
235 - def _split(self,var,delim):
236 if var: 237 return var.split(delim) 238 else: 239 return []
240
241 - def _parse_file(self,opts):
242 regex = re.compile(r'^(BUG|bug|TYPE|type|CVE|cve)=(.*$)') 243 types = {'S':'security','B':'bugfix','E':'enhancement'} 244 notes = self._split(opts.notes,'\n') 245 bugs = self._split(opts.bugs,',') 246 cves = self._split(opts.cves,',') 247 print "Reading from %s " % opts.input_file 248 if os.path.exists(opts.input_file): 249 f = open(opts.input_file) 250 lines = f.readlines() 251 f.close() 252 for line in lines: 253 if line[0] == ':' or line[0] == '#': 254 continue 255 src=regex.search(line) 256 if src: 257 cmd,para = tuple(src.groups()) 258 cmd=cmd.upper() 259 if cmd == 'BUG': 260 para = [p for p in para.split(' ')] 261 bugs.extend(para) 262 elif cmd == 'CVE': 263 para = [p for p in para.split(' ')] 264 cves.extend(para) 265 elif cmd == 'TYPE': 266 opts.type = types[para.upper()] 267 268 else: # This is notes 269 notes.append(line[:-1]) 270 if notes: 271 opts.notes = "\r\n".join(notes) 272 if bugs: 273 opts.bugs = ','.join(bugs) 274 if cves: 275 opts.cves = ','.join(cves) 276 log.debug("Type : %s" % opts.type) 277 log.debug('Bugs:\n%s' % opts.bugs) 278 log.debug('CVES:\n%s' % opts.cves) 279 log.debug('Notes:\n%s' % opts.notes)
280 281 if __name__ == '__main__': 282 usage = "usage: %prog [options]" 283 parser = OptionParser(usage, description=__description__) 284 285 ## Actions 286 parser.add_option("-n", "--new", action="store", type="string", dest="new", 287 help="Add a new update to the system (--new=foo-1.2-3," 288 "bar-4.5-6)") 289 parser.add_option("-m", "--masher", action="store_true", dest="masher", 290 help="Display the status of the Masher") 291 parser.add_option("-p", "--push", action="store_true", dest="push", 292 help="Display and push any pending updates") 293 294 # --edit ? 295 296 ## Details 297 parser.add_option("-s", "--status", action="store", type="string", 298 dest="status", help="List [testing|pending|requests|" 299 "stable|security] updates") 300 parser.add_option("-b", "--bugs", action="store", type="string", 301 dest="bugs", help="Associate bugs with an update " 302 "(--bugs=1234,5678)", default="") 303 parser.add_option("-c", "--cves", action="store", type="string", 304 dest="cves", help="A list of comma-separated CVE IDs", 305 default="") 306 parser.add_option("-r", "--release", action="store", type="string", 307 dest="release", help="Release (default: F7)", 308 default="F7") 309 parser.add_option("-N", "--notes", action="store", type="string", 310 dest="notes", help="Update notes", default="") 311 parser.add_option("-t", "--type", action="store", type="string", 312 dest="type", 313 help="Update type [bugfix|security|enhancement] " 314 "(default: bugfix)") 315 parser.add_option("", "--file", action="store", type="string", 316 dest="input_file", 317 help="Get Bugs,CVES,Notes from a file") 318 319 # --package 320 # --build (or just take these values from args) 321 322 ## Update actions 323 #parser.add_option("-u", "--unpush", action="store", type="string", 324 # dest="unpush", help="Unpush a given update", 325 # metavar="UPDATE") 326 #parser.add_option("-f", "--feedback", action="store", type="string", 327 # dest="feedback", metavar="UPDATE", 328 # help="Give [-1|0|1] feedback about an update") 329 #parser.add_option("-C", "--comment", action="store", type="string", 330 # dest="comment", metavar="UPDATE", 331 # help="Comment about an update") 332 parser.add_option("-S", "--stable", action="store", type="string", 333 dest="stable", metavar="UPDATE", 334 help="Mark an update for push to stable") 335 parser.add_option("-T", "--testing", action="store", type="string", 336 dest="testing", metavar="UPDATE", 337 help="Mark an update for push to testing") 338 parser.add_option("-d", "--delete", action="store", type="string", 339 dest="delete", help="Delete an update", 340 metavar="UPDATE") 341 342 parser.add_option("-v", "--verbose", action="store_true", dest="verbose", 343 help="Show debugging messages") 344 parser.add_option("-l", "--limit", action="store", type="int", dest="limit", 345 default=10, help="Maximum number of updates to return " 346 "(default: 10)") 347 348 (opts, args) = parser.parse_args() 349 350 # Setup the logger 351 sh = logging.StreamHandler() 352 if opts.verbose: 353 log.setLevel(logging.DEBUG) 354 sh.setLevel(logging.DEBUG) 355 else: 356 log.setLevel(logging.INFO) 357 sh.setLevel(logging.INFO) 358 format = logging.Formatter("%(message)s") 359 sh.setFormatter(format) 360 log.addHandler(sh) 361 362 BodhiClient(opts) 363