1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
43
45 """
46 A command-line client to interact with Bodhi.
47 """
48
49 session = None
50
68
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
109 """
110 Store a pickled session cookie.
111 """
112 s = file(SESSION_FILE, 'w')
113 pickle.dump(self.session, s)
114 s.close()
115
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
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
138 data = None
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):
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
196
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
204 data = self.send_request('move', nvr=opts.stable, auth=True)
205 log.info(data['tg_flash'])
206
207
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
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)
236 if var:
237 return var.split(delim)
238 else:
239 return []
240
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:
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
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
295
296
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
320
321
322
323
324
325
326
327
328
329
330
331
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
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