Repository: buildbot/buildbot

Issue:
Fix issue with the table 'scheduler_masters'
This table is populated by the SchedulersConnectorComponent::setSchedulerMaster, but deleted by the function MastersConnectorComponent::setMasterState even if the master is active.

If the master is active, the content of this table mustn't be deleted, otherwise schedulers aren't link to a master. As a consequence, when requesting
- /api/v2/masters/<master_id>/schedulers/<sched_id>, the master field is always null. 
- /api/v2/schedulers, the master field is always null.
  - the Gui page about schedulers (http://127.0.0.1:8020/#/schedulers) the column Master is empty



Discussion:

Resolving patch:
diff --git a/master/buildbot/clients/tryclient.py b/master/buildbot/clients/tryclient.py
--- a/master/buildbot/clients/tryclient.py
+++ b/master/buildbot/clients/tryclient.py
@@ -335,18 +335,33 @@ def ns(s):
     return "%d:%s," % (len(s), s)
 
 def createJobfile(bsid, branch, baserev, patchlevel, diff, repository, 
-                  project, builderNames):
-    job = ""
-    job += ns("2")
-    job += ns(bsid)
-    job += ns(branch)
-    job += ns(str(baserev))
-    job += ns("%d" % patchlevel)
-    job += ns(diff)
-    job += ns(repository)
-    job += ns(project)
-    for bn in builderNames:
-        job += ns(bn)
+                  project, who, builderNames):
+    job = None
+    if who:
+        job = ""
+        job += ns("3")
+        job += ns(bsid)
+        job += ns(branch)
+        job += ns(str(baserev))
+        job += ns("%d" % patchlevel)
+        job += ns(diff)
+        job += ns(repository)
+        job += ns(project)
+        job += ns(who)
+        for bn in builderNames:
+            job += ns(bn)
+    else:
+        job = ""
+        job += ns("2")
+        job += ns(bsid)
+        job += ns(branch)
+        job += ns(str(baserev))
+        job += ns("%d" % patchlevel)
+        job += ns(diff)
+        job += ns(repository)
+        job += ns(project)
+        for bn in builderNames:
+            job += ns(bn)
     return job
 
 def getTopdir(topfile, start=None):
@@ -432,6 +447,7 @@ def __init__(self, config):
         assert self.connect, "you must specify a connect style: ssh or pb"
         self.builderNames = self.getopt('builders')
         self.project = self.getopt('project', '')
+        self.who = self.getopt('who')
 
     def getopt(self, config_name, default=None):
         value = self.config.get(config_name)
@@ -490,7 +506,8 @@ def _createJob_1(self, ss):
             self.jobfile = createJobfile(self.bsid,
                                          ss.branch or "", revspec,
                                          patchlevel, diff, ss.repository,
-                                         self.project, self.builderNames)
+                                         self.project, self.who, 
+                                         self.builderNames)
 
     def fakeDeliverJob(self):
         # Display the job to be delivered, but don't perform delivery.
@@ -538,14 +555,25 @@ def deliverJob(self):
     def _deliverJob_pb(self, remote):
         ss = self.sourcestamp
 
-        d = remote.callRemote("try",
-                              ss.branch,
-                              ss.revision,
-                              ss.patch,
-                              ss.repository,
-                              self.project,
-                              self.builderNames,
-                              self.config.get('properties', {}))
+        if self.who:
+            d = remote.callRemote("try",
+                                  ss.branch,
+                                  ss.revision,
+                                  ss.patch,
+                                  ss.repository,
+                                  self.project,
+                                  self.builderNames,
+                                  self.who,
+                                  self.config.get('properties', {}))
+        else:
+            d = remote.callRemote("try",
+                                  ss.branch,
+                                  ss.revision,
+                                  ss.patch,
+                                  ss.repository,
+                                  self.project,
+                                  self.builderNames,
+                                  self.config.get('properties', {}))
         d.addCallback(self._deliverJob_pb2)
         return d
     def _deliverJob_pb2(self, status):
diff --git a/master/buildbot/schedulers/trysched.py b/master/buildbot/schedulers/trysched.py
--- a/master/buildbot/schedulers/trysched.py
+++ b/master/buildbot/schedulers/trysched.py
@@ -116,6 +116,7 @@ def parseJob(self, f):
         if not p.strings:
             raise BadJobfile("could not find any complete netstrings")
         ver = p.strings.pop(0)
+
         if ver == "1":
             buildsetID, branch, baserev, patchlevel, diff = p.strings[:5]
             builderNames = p.strings[5:]
@@ -126,6 +127,7 @@ def parseJob(self, f):
             patchlevel = int(patchlevel)
             repository=''
             project=''
+            who=''
         elif ver == "2": # introduced the repository and project property
             buildsetID, branch, baserev, patchlevel, diff, repository, project = p.strings[:7]
             builderNames = p.strings[7:]
@@ -134,6 +136,15 @@ def parseJob(self, f):
             if baserev == "":
                 baserev = None
             patchlevel = int(patchlevel)
+            who=''
+        elif ver == "3": # introduced who property
+            buildsetID, branch, baserev, patchlevel, diff, repository, project, who = p.strings[:8]
+            builderNames = p.strings[8:]
+            if branch == "":
+                branch = None
+            if baserev == "":
+                baserev = None
+            patchlevel = int(patchlevel)
         else:
             raise BadJobfile("unknown version '%s'" % ver)
         return dict(
@@ -144,6 +155,7 @@ def parseJob(self, f):
                 patch_level=patchlevel,
                 repository=repository,
                 project=project,
+                who=who,
                 jobid=buildsetID)
 
     def handleJobFile(self, filename, f):
@@ -170,8 +182,11 @@ def handleJobFile(self, filename, f):
                 project=parsed_job['project'],
                 repository=parsed_job['repository'])
         def create_buildset(ssid):
+            reason = "'try' job"
+            if parsed_job['who']:
+                reason += " by user %s" % parsed_job['who']
             return self.addBuildsetForSourceStamp(ssid=ssid,
-                    reason="'try' job", external_idstring=parsed_job['jobid'],
+                    reason=reason, external_idstring=parsed_job['jobid'],
                     builderNames=builderNames)
         d.addCallback(create_buildset)
         return d
@@ -184,7 +199,7 @@ def __init__(self, scheduler, username):
 
     @defer.deferredGenerator
     def perspective_try(self, branch, revision, patch, repository, project,
-                        builderNames, properties={}, ):
+                        builderNames, who='', properties={} ):
         db = self.scheduler.master.db
         log.msg("user %s requesting build on builders %s" % (self.username,
                                                              builderNames))
@@ -202,7 +217,9 @@ def perspective_try(self, branch, revision, patch, repository, project,
         yield wfd
         ssid = wfd.getResult()
 
-        reason = "'try' job from user %s" % self.username
+        reason = "'try' job"
+        if who:
+            reason += " by user %s" % who
 
         requested_props = Properties()
         requested_props.update(properties, "try build")
diff --git a/master/buildbot/scripts/runner.py b/master/buildbot/scripts/runner.py
--- a/master/buildbot/scripts/runner.py
+++ b/master/buildbot/scripts/runner.py
@@ -940,6 +940,8 @@ class TryOptions(OptionsWithOptionsFile):
          "Location of the buildmaster's PBListener (host:port)"],
         ["passwd", None, None,
          "Password for PB authentication"],
+        ["who", "w", None,
+         "Who is responsible for the try build"],
 
         ["diff", None, None,
          "Filename of a patch to use instead of scanning a local tree. "
@@ -999,6 +1001,7 @@ class TryOptions(OptionsWithOptionsFile):
         [ 'try_jobdir', 'jobdir' ],
         [ 'try_password', 'passwd' ],
         [ 'try_master', 'master' ],
+        [ 'try_who', 'who' ],
         #[ 'try_wait', 'wait' ], <-- handled in postOptions
         [ 'try_masterstatus', 'masterstatus' ],
         # Deprecated command mappings from the quirky old days:


Test patch:
diff --git a/master/buildbot/test/unit/test_schedulers_trysched.py b/master/buildbot/test/unit/test_schedulers_trysched.py
--- a/master/buildbot/test/unit/test_schedulers_trysched.py
+++ b/master/buildbot/test/unit/test_schedulers_trysched.py
@@ -153,6 +153,7 @@ def test_parseJob_v1(self):
             'patch_body': 'this is my diff, -- ++, etc.',
             'patch_level': 1,
             'project': '',
+            'who': '',
             'repository': ''
         })
 
@@ -185,6 +186,7 @@ def test_parseJob_v2(self):
             'patch_body': 'this is my diff, -- ++, etc.',
             'patch_level': 1,
             'project': 'proj',
+            'who': '',
             'repository': 'repo'
         })
 
@@ -211,6 +213,50 @@ def test_parseJob_v2_no_builders(self):
         parsedjob = sched.parseJob(StringIO.StringIO(jobstr))
         self.assertEqual(parsedjob['builderNames'], [])
 
+    def test_parseJob_v3(self):
+        sched = trysched.Try_Jobdir(name='tsched',
+                builderNames=['buildera','builderb'], jobdir='foo')
+        jobstr = self.makeNetstring(
+            '3', 'extid', 'trunk', '1234', '1', 'this is my diff, -- ++, etc.',
+            'repo', 'proj', 'who',
+            'buildera', 'builderc'
+        )
+        parsedjob = sched.parseJob(StringIO.StringIO(jobstr))
+        self.assertEqual(parsedjob, {
+            'baserev': '1234',
+            'branch': 'trunk',
+            'builderNames': ['buildera', 'builderc'],
+            'jobid': 'extid',
+            'patch_body': 'this is my diff, -- ++, etc.',
+            'patch_level': 1,
+            'project': 'proj',
+            'who': 'who',
+            'repository': 'repo'
+        })
+
+    def test_parseJob_v3_empty_branch_rev(self):
+        sched = trysched.Try_Jobdir(name='tsched',
+                builderNames=['buildera','builderb'], jobdir='foo')
+        jobstr = self.makeNetstring(
+                # blank branch, rev are turned to None
+            '3', 'extid', '', '', '1', 'this is my diff, -- ++, etc.',
+            'repo', 'proj', 'who',
+            'buildera', 'builderc'
+        )
+        parsedjob = sched.parseJob(StringIO.StringIO(jobstr))
+        self.assertEqual(parsedjob['branch'], None)
+        self.assertEqual(parsedjob['baserev'], None)
+
+    def test_parseJob_v3_no_builders(self):
+        sched = trysched.Try_Jobdir(name='tsched',
+                builderNames=['buildera','builderb'], jobdir='foo')
+        jobstr = self.makeNetstring(
+            '3', 'extid', 'trunk', '1234', '1', 'this is my diff, -- ++, etc.',
+            'repo', 'proj', 'who'
+        )
+        parsedjob = sched.parseJob(StringIO.StringIO(jobstr))
+        self.assertEqual(parsedjob['builderNames'], [])
+
     # handleJobFile
 
     def call_handleJobFile(self, parseJob):
@@ -230,7 +276,7 @@ def makeSampleParsedJob(self, **overrides):
         pj = dict(baserev='1234', branch='trunk',
             builderNames=['buildera', 'builderb'],
             jobid='extid', patch_body='this is my diff, -- ++, etc.',
-            patch_level=1, project='proj', repository='repo')
+            patch_level=1, project='proj', repository='repo', who='who')
         pj.update(overrides)
         return pj
 
@@ -238,7 +284,7 @@ def test_handleJobFile(self):
         d = self.call_handleJobFile(lambda f : self.makeSampleParsedJob())
         def check(_):
             self.db.buildsets.assertBuildset('?',
-                    dict(reason="'try' job",
+                    dict(reason="'try' job by user who",
                         external_idstring='extid',
                         properties=[('scheduler', ('tsched', 'Scheduler'))]),
                     dict(branch='trunk', repository='repo',
@@ -274,7 +320,7 @@ def test_handleJobFile_subset_builders(self):
                 lambda f : self.makeSampleParsedJob(builderNames=['buildera']))
         def check(_):
             self.db.buildsets.assertBuildset('?',
-                    dict(reason="'try' job",
+                    dict(reason="'try' job by user who",
                         external_idstring='extid',
                         properties=[('scheduler', ('tsched', 'Scheduler'))]),
                     dict(branch='trunk', repository='repo',
@@ -318,7 +364,25 @@ def test_perspective_try(self):
                 'proj', ['a'], properties={'pr':'op'})
         def check(_):
             self.db.buildsets.assertBuildset('?',
-                    dict(reason="'try' job from user a",
+                    dict(reason="'try' job",
+                        external_idstring=None,
+                        properties=[
+                            ('frm', ('schd', 'Scheduler')),
+                            ('pr', ('op', 'try build')),
+                            ('scheduler', ('tsched', 'Scheduler')),
+                        ]),
+                    dict(branch='default', repository='repo',
+                        project='proj', revision='abcdef',
+                        patch_body='-- ++', patch_level=1, patch_subdir=''))
+        d.addCallback(check)
+        return d
+
+    def test_perspective_try_who(self):
+        d = self.call_perspective_try('default', 'abcdef', (1, '-- ++'), 'repo',
+                'proj', ['a'], who='who', properties={'pr':'op'})
+        def check(_):
+            self.db.buildsets.assertBuildset('?',
+                    dict(reason="'try' job by user who",
                         external_idstring=None,
                         properties=[
                             ('frm', ('schd', 'Scheduler')),