prscript.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #!/usr/bin/env python3
  2. # Copyright(C) 2013-2020 Open Information Security Foundation
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, version 2 of the License.
  6. #
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License
  13. # along with this program; if not, write to the Free Software
  14. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  15. # Note to Docker users:
  16. # If you are running SELinux in enforced mode, you may want to run
  17. # chcon -Rt svirt_sandbox_file_t SURICATA_ROOTSRC_DIR
  18. # or the buildbot will not be able to access to the data in /data/oisf
  19. # and the git step will fail.
  20. import urllib.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse, http.cookiejar
  21. try:
  22. import simplejson as json
  23. except:
  24. import json
  25. import time
  26. import argparse
  27. import sys
  28. import os
  29. import copy
  30. from subprocess import Popen, PIPE
  31. GOT_NOTIFY = True
  32. try:
  33. import pynotify
  34. except:
  35. GOT_NOTIFY = False
  36. GOT_DOCKER = True
  37. GOT_DOCKERPY_API = 2
  38. try:
  39. import docker
  40. try:
  41. from docker import APIClient as Client
  42. from docker import from_env as DockerClient
  43. except ImportError:
  44. GOT_DOCKERPY_API = 1
  45. try:
  46. from docker import Client
  47. except ImportError:
  48. GOT_DOCKER = False
  49. except ImportError:
  50. GOT_DOCKER = False
  51. # variables
  52. # - github user
  53. # - buildbot user and password
  54. BASE_URI="https://buildbot.openinfosecfoundation.org/"
  55. GITHUB_BASE_URI = "https://api.github.com/repos/"
  56. GITHUB_MASTER_URI = "https://api.github.com/repos/OISF/suricata/commits?sha=master"
  57. if GOT_DOCKER:
  58. parser = argparse.ArgumentParser(prog='prscript', description='Script checking validity of branch before PR')
  59. else:
  60. parser = argparse.ArgumentParser(prog='prscript', description='Script checking validity of branch before PR',
  61. epilog='You need to install Python docker module to enable docker container handling options.')
  62. parser.add_argument('-u', '--username', dest='username', help='github and buildbot user')
  63. parser.add_argument('-p', '--password', dest='password', help='buildbot password')
  64. parser.add_argument('-c', '--check', action='store_const', const=True, help='only check last build', default=False)
  65. parser.add_argument('-v', '--verbose', action='store_const', const=True, help='verbose output', default=False)
  66. parser.add_argument('--norebase', action='store_const', const=True, help='do not test if branch is in sync with master', default=False)
  67. parser.add_argument('-r', '--repository', dest='repository', default='suricata', help='name of suricata repository on github')
  68. parser.add_argument('-l', '--local', action='store_const', const=True, help='local testing before github push', default=False)
  69. if GOT_NOTIFY:
  70. parser.add_argument('-n', '--notify', action='store_const', const=True, help='send desktop notification', default=False)
  71. docker_deps = ""
  72. if not GOT_DOCKER:
  73. docker_deps = " (disabled)"
  74. parser.add_argument('-d', '--docker', action='store_const', const=True, help='use docker based testing', default=False)
  75. parser.add_argument('-C', '--create', action='store_const', const=True, help='create docker container' + docker_deps, default=False)
  76. parser.add_argument('-s', '--start', action='store_const', const=True, help='start docker container' + docker_deps, default=False)
  77. parser.add_argument('-S', '--stop', action='store_const', const=True, help='stop docker container' + docker_deps, default=False)
  78. parser.add_argument('-R', '--rm', action='store_const', const=True, help='remove docker container and image' + docker_deps, default=False)
  79. parser.add_argument('branch', metavar='branch', help='github branch to build', nargs='?')
  80. args = parser.parse_args()
  81. username = args.username
  82. password = args.password
  83. cookie = None
  84. if args.create or args.start or args.stop or args.rm:
  85. if GOT_DOCKER:
  86. args.docker = True
  87. args.local = True
  88. else:
  89. print("You need to install python docker to use docker handling features.")
  90. sys.exit(-1)
  91. if not args.local:
  92. if not args.username:
  93. print("You need to specify a github username (-u option) for this mode (or use -l to disable)")
  94. sys.exit(-1)
  95. if args.docker:
  96. BASE_URI="http://localhost:8010/"
  97. BUILDERS_LIST = ["gcc", "clang", "debug", "features", "profiling", "pcaps"]
  98. else:
  99. BUILDERS_LIST = [username, username + "-pcap"]
  100. BUILDERS_URI=BASE_URI+"builders/"
  101. JSON_BUILDERS_URI=BASE_URI+"json/builders/"
  102. if GOT_NOTIFY:
  103. if args.notify:
  104. pynotify.init("PRscript")
  105. def SendNotification(title, text):
  106. if not GOT_NOTIFY:
  107. return
  108. if not args.notify:
  109. return
  110. n = pynotify.Notification(title, text)
  111. n.show()
  112. def TestRepoSync(branch):
  113. request = urllib.request.Request(GITHUB_MASTER_URI)
  114. page = urllib.request.urlopen(request)
  115. json_result = json.loads(page.read())
  116. sha_orig = json_result[0]["sha"]
  117. check_command = ["git", "branch", "--contains", sha_orig ]
  118. p1 = Popen(check_command, stdout=PIPE)
  119. p2 = Popen(["grep", branch], stdin=p1.stdout, stdout=PIPE)
  120. p1.stdout.close()
  121. output = p2.communicate()[0]
  122. if len(output) == 0:
  123. return -1
  124. return 0
  125. def TestGithubSync(branch):
  126. request = urllib.request.Request(GITHUB_BASE_URI + username + "/" + args.repository + "/commits?sha=" + branch + "&per_page=1")
  127. try:
  128. page = urllib.request.urlopen(request)
  129. except urllib.error.HTTPError as e:
  130. if e.code == 404:
  131. return -2
  132. else:
  133. raise(e)
  134. json_result = json.loads(page.read())
  135. sha_github = json_result[0]["sha"]
  136. check_command = ["git", "rev-parse", branch]
  137. p1 = Popen(check_command, stdout=PIPE)
  138. sha_local = p1.communicate()[0].decode('ascii').rstrip()
  139. if sha_local != sha_github:
  140. return -1
  141. return 0
  142. def OpenBuildbotSession():
  143. auth_params = { 'username':username,'passwd':password, 'name':'login'}
  144. cookie = http.cookiejar.LWPCookieJar()
  145. params = urllib.parse.urlencode(auth_params).encode('ascii')
  146. opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie))
  147. urllib.request.install_opener(opener)
  148. request = urllib.request.Request(BASE_URI + 'login', params)
  149. _ = urllib.request.urlopen(request)
  150. return cookie
  151. def SubmitBuild(branch, extension = "", builder_name = None):
  152. raw_params = {'branch':branch,'reason':'Testing ' + branch, 'name':'force_build', 'forcescheduler':'force'}
  153. params = urllib.parse.urlencode(raw_params).encode('ascii')
  154. if not args.docker:
  155. opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie))
  156. urllib.request.install_opener(opener)
  157. if builder_name == None:
  158. builder_name = username + extension
  159. request = urllib.request.Request(BUILDERS_URI + builder_name + '/force', params)
  160. page = urllib.request.urlopen(request)
  161. result = page.read()
  162. if args.verbose:
  163. print("=== response ===")
  164. print(result)
  165. print("=== end of response ===")
  166. if args.docker:
  167. if "<h2>Pending Build Requests:</h2>" in result:
  168. print("Build '" + builder_name + "' submitted")
  169. return 0
  170. else:
  171. return -1
  172. if b'Current Builds' in result:
  173. print("Build '" + builder_name + "' submitted")
  174. return 0
  175. else:
  176. return -1
  177. # TODO honor the branch argument
  178. def FindBuild(branch, extension = "", builder_name = None):
  179. if builder_name == None:
  180. request = urllib.request.Request(JSON_BUILDERS_URI + username + extension + '/')
  181. else:
  182. request = urllib.request.Request(JSON_BUILDERS_URI + builder_name + '/')
  183. page = urllib.request.urlopen(request)
  184. json_result = json.loads(page.read())
  185. # Pending build is unnumbered
  186. if json_result["pendingBuilds"]:
  187. return -1
  188. if json_result["currentBuilds"]:
  189. return json_result["currentBuilds"][0]
  190. if json_result["cachedBuilds"]:
  191. return json_result["cachedBuilds"][-1]
  192. return -2
  193. def GetBuildStatus(builder, buildid, extension="", builder_name = None):
  194. if builder_name == None:
  195. builder_name = username + extension
  196. # https://buildbot.oisf.net/json/builders/build%20deb6/builds/11
  197. request = urllib.request.Request(JSON_BUILDERS_URI + builder_name + '/builds/' + str(buildid))
  198. page = urllib.request.urlopen(request)
  199. result = page.read()
  200. if args.verbose:
  201. print("=== response ===")
  202. print(result)
  203. print("=== end of response ===")
  204. json_result = json.loads(result)
  205. if json_result["currentStep"]:
  206. return 1
  207. if 'successful' in json_result["text"]:
  208. return 0
  209. return -1
  210. def WaitForBuildResult(builder, buildid, extension="", builder_name = None):
  211. # fetch result every 10 secs till task is over
  212. if builder_name == None:
  213. builder_name = username + extension
  214. res = 1
  215. while res == 1:
  216. res = GetBuildStatus(username,buildid, builder_name = builder_name)
  217. if res == 1:
  218. time.sleep(10)
  219. # return the result
  220. if res == 0:
  221. print("Build successful for " + builder_name)
  222. else:
  223. print("Build failure for " + builder_name + ": " + BUILDERS_URI + builder_name + '/builds/' + str(buildid))
  224. return res
  225. # check that github branch and OISF master branch are sync
  226. if not args.local:
  227. ret = TestGithubSync(args.branch)
  228. if ret != 0:
  229. if ret == -2:
  230. print("Branch " + args.branch + " is not pushed to Github.")
  231. sys.exit(-1)
  232. if args.norebase:
  233. print("Branch " + args.branch + " is not in sync with corresponding Github branch. Continuing due to --norebase option.")
  234. else:
  235. print("Branch " + args.branch + " is not in sync with corresponding Github branch. Push may be needed.")
  236. sys.exit(-1)
  237. if TestRepoSync(args.branch) != 0:
  238. if args.norebase:
  239. print("Branch " + args.branch + " is not in sync with OISF's master branch. Continuing due to --norebase option.")
  240. else:
  241. print("Branch " + args.branch + " is not in sync with OISF's master branch. Rebase needed.")
  242. sys.exit(-1)
  243. def CreateContainer():
  244. # FIXME check if existing
  245. print("Pulling docking image, first run should take long")
  246. if GOT_DOCKERPY_API < 2:
  247. cli = Client()
  248. cli.pull('regit/suri-buildbot')
  249. cli.create_container(name='suri-buildbot', image='regit/suri-buildbot', ports=[8010, 22], volumes=['/data/oisf', '/data/buildbot/master/master.cfg'])
  250. else:
  251. cli = DockerClient()
  252. cli.images.pull('regit/suri-buildbot')
  253. suri_src_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
  254. print("Using base src dir: " + suri_src_dir)
  255. cli.containers.create('regit/suri-buildbot', name='suri-buildbot', ports={'8010/tcp': 8010, '22/tcp': None} , volumes={suri_src_dir: { 'bind': '/data/oisf', 'mode': 'ro'}, os.path.join(suri_src_dir,'qa','docker','buildbot.cfg'): { 'bind': '/data/buildbot/master/master.cfg', 'mode': 'ro'}}, detach = True)
  256. sys.exit(0)
  257. def StartContainer():
  258. suri_src_dir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
  259. print("Using base src dir: " + suri_src_dir)
  260. if GOT_DOCKERPY_API < 2:
  261. cli = Client()
  262. cli.start('suri-buildbot', port_bindings={8010:8010, 22:None}, binds={suri_src_dir: { 'bind': '/data/oisf', 'ro': True}, os.path.join(suri_src_dir,'qa','docker','buildbot.cfg'): { 'bind': '/data/buildbot/master/master.cfg', 'ro': True}} )
  263. else:
  264. cli = DockerClient()
  265. cli.containers.get('suri-buildbot').start()
  266. sys.exit(0)
  267. def StopContainer():
  268. if GOT_DOCKERPY_API < 2:
  269. cli = Client()
  270. cli.stop('suri-buildbot')
  271. else:
  272. cli = DockerClient()
  273. cli.containers.get('suri-buildbot').stop()
  274. sys.exit(0)
  275. def RmContainer():
  276. if GOT_DOCKERPY_API < 2:
  277. cli = Client()
  278. try:
  279. cli.remove_container('suri-buildbot')
  280. except:
  281. print("Unable to remove suri-buildbot container")
  282. try:
  283. cli.remove_image('regit/suri-buildbot:latest')
  284. except:
  285. print("Unable to remove suri-buildbot images")
  286. else:
  287. cli = DockerClient()
  288. cli.containers.get('suri-buildbot').remove()
  289. cli.images.remove('regit/suri-buildbot:latest')
  290. sys.exit(0)
  291. if GOT_DOCKER:
  292. if args.create:
  293. CreateContainer()
  294. if args.start:
  295. StartContainer()
  296. if args.stop:
  297. StopContainer()
  298. if args.rm:
  299. RmContainer()
  300. if not args.branch:
  301. print("You need to specify a branch for this mode")
  302. sys.exit(-1)
  303. # submit buildbot form to build current branch on the devel builder
  304. if not args.check:
  305. if not args.docker:
  306. cookie = OpenBuildbotSession()
  307. if cookie == None:
  308. print("Unable to connect to buildbot with provided credentials")
  309. sys.exit(-1)
  310. for build in BUILDERS_LIST:
  311. res = SubmitBuild(args.branch, builder_name = build)
  312. if res == -1:
  313. print("Unable to start build. Check command line parameters")
  314. sys.exit(-1)
  315. buildids = {}
  316. if args.docker:
  317. time.sleep(2)
  318. # get build number and exit if we don't have
  319. for build in BUILDERS_LIST:
  320. buildid = FindBuild(args.branch, builder_name = build)
  321. if buildid == -1:
  322. print("Pending build tracking is not supported. Follow build by browsing " + BUILDERS_URI + build)
  323. elif buildid == -2:
  324. print("No build found for " + BUILDERS_URI + build)
  325. sys.exit(0)
  326. else:
  327. if not args.docker:
  328. print("You can watch build progress at " + BUILDERS_URI + build + "/builds/" + str(buildid))
  329. buildids[build] = buildid
  330. if args.docker:
  331. print("You can watch build progress at " + BASE_URI + "waterfall")
  332. if len(buildids):
  333. print("Waiting for build completion")
  334. else:
  335. sys.exit(0)
  336. buildres = 0
  337. if args.docker:
  338. while len(buildids):
  339. up_buildids = copy.copy(buildids)
  340. for build in buildids:
  341. ret = GetBuildStatus(build, buildids[build], builder_name = build)
  342. if ret == -1:
  343. buildres = -1
  344. up_buildids.pop(build, None)
  345. if len(up_buildids):
  346. remains = " (remaining builds: " + ', '.join(list(up_buildids.keys())) + ")"
  347. else:
  348. remains = ""
  349. print("Build failure for " + build + ": " + BUILDERS_URI + build + '/builds/' + str(buildids[build]) + remains)
  350. elif ret == 0:
  351. up_buildids.pop(build, None)
  352. if len(up_buildids):
  353. remains = " (remaining builds: " + ', '.join(list(up_buildids.keys())) + ")"
  354. else:
  355. remains = ""
  356. print("Build successful for " + build + remains)
  357. time.sleep(5)
  358. buildids = up_buildids
  359. if res == -1:
  360. SendNotification("PRscript failure", "Some builds have failed. Check <a href='" + BASE_URI + "waterfall'>waterfall</a> for results.")
  361. sys.exit(-1)
  362. else:
  363. print("PRscript completed successfully")
  364. SendNotification("PRscript success", "Congrats! All builds have passed.")
  365. sys.exit(0)
  366. else:
  367. for build in buildids:
  368. res = WaitForBuildResult(build, buildids[build], builder_name = build)
  369. if res == -1:
  370. buildres = -1
  371. if buildres == 0:
  372. if not args.norebase and not args.docker:
  373. print("You can copy/paste following lines into github PR")
  374. for build in buildids:
  375. print("- PR " + build + ": " + BUILDERS_URI + build + "/builds/" + str(buildids[build]))
  376. SendNotification("OISF PRscript success", "Congrats! All builds have passed.")
  377. sys.exit(0)
  378. else:
  379. SendNotification("OISF PRscript failure", "Some builds have failed. Check <a href='" + BASE_URI + "waterfall'>waterfall</a> for results.")
  380. sys.exit(-1)