def _scratch_build(self, session: koji.ClientSession, name: str, source: str) -> int: """ Uploads source RPM and starts scratch build of package in Koji. Params: session: Koji session to use for starting build. name: Name of the package to build source: Path to SRPM. Returns: Build id. """ _logger.info("Uploading {source} to koji".format(source=source)) suffix = "".join( [random.choice(string.ascii_letters) for i in range(8)]) serverdir = "%s/%r.%s" % ("cli-build", time.time(), suffix) session.uploadWrapper(source, serverdir) remote = "%s/%s" % (serverdir, os.path.basename(source)) _logger.info("Intiating koji build for %r" % dict( name=name, target=self.target_tag, source=remote, opts=self.opts)) task_id = session.build(remote, self.target_tag, self.opts, priority=self.priority) _logger.info("Scratch build created for {name}: {url}".format( name=name, url=self.web_url + "/taskinfo?taskID={}".format(task_id))) return task_id
def list_buildroot(session: koji.ClientSession, nvr: str) -> List[str]: build = session.getBuild(nvr, strict=True) if build["state"] != koji.BUILD_STATES["COMPLETE"]: raise Exception("Build is not yet complete.") task = session.listTasks( opts={ "method": "buildArch", "parent": build["task_id"], }, queryOpts={"limit": 1} )[0] buildroot = session.listBuildroots( taskID=task["id"], queryOpts={"order": "-id", "limit": 1} )[0] rpms = session.listRPMs(componentBuildrootID=buildroot["id"]) with session.multicall(strict=True) as multi: srpms = [ multi.listRPMs( buildID=rpm['build_id'], arches='src', queryOpts={'limit': 1} ) for rpm in rpms if rpm['name'].startswith('rust-') and rpm['name'].endswith('-devel') ] nvrs = set(data.result[0]['nvr'] for data in srpms) return [*sorted(nvrs)]
def get_koji_builds(self, ) -> Dict: """ Get latest koji builds as a dict of branch: latest build in that branch. """ session = ClientSession( baseurl="https://koji.fedoraproject.org/kojihub") package_id = session.getPackageID( self.dg.package_config.downstream_package_name) # This method returns only latest builds, # so we don't need to get whole build history from Koji, # get just recent year to speed things up. since = datetime.now() - timedelta(days=365) builds_l = session.listBuilds( packageID=package_id, state=BUILD_STATES["COMPLETE"], completeAfter=since.timestamp(), ) logger.debug( f"Recent Koji builds fetched: {[b['nvr'] for b in builds_l]}") # Select latest build for each branch. # [{'nvr': 'python-ogr-0.5.0-1.fc29'}, {'nvr':'python-ogr-0.6.0-1.fc29'}] # -> {'fc29': 'python-ogr-0.6.0-1.fc29'} return { b["nvr"].rsplit(".", 1)[1]: b["nvr"] for b in reversed(builds_l) }
def cli_filter_tags(session: ClientSession, tag_list: List[Union[int, str]], search: Optional[str] = None, regex: Optional[str] = None, tag_sifter: Optional[Sifter] = None, sorting: Optional[str] = None, outputs: Optional[dict] = None, strict: bool = False): if search: tag_list.extend(t["id"] for t in session.search(search, "tag", "glob") if t) if regex: tag_list.extend(t["id"] for t in session.search(regex, "tag", "regex") if t) tag_list = unique(map(int_or_str, tag_list)) loaded = bulk_load_tags(session, tag_list, err=strict) tags = tag_dedup(loaded.values()) if tag_sifter: results = tag_sifter(session, tags) else: results = {"default": list(tags)} if sorting == SORT_BY_NAME: sortkey = "name" elif sorting == SORT_BY_ID: sortkey = "id" else: sortkey = None # unsure why output_sifted(results, "name", outputs, sort=sortkey) # type: ignore
def gather_affected_targets(session: ClientSession, tagnames: Iterable[TagSpec]) -> List[TargetInfo]: """ Returns the list of target info dicts representing the targets which inherit any of the given named tags. That is to say, the targets whose build tags are children of the named tags. This list allows us to gauge what build configurations would be impacted by changes to the given tags. :param tagnames: List of tag names :raises NoSuchTag: if any of the names do not resolve to a tag info :since: 1.0 """ tags = [as_taginfo(session, t) for t in set(tagnames)] ifn = lambda tag: session.getFullInheritance(tag['id'], reverse=True) loaded = bulk_load(session, ifn, tags) parents = filter(None, loaded.values()) tagids = set(chain(*((ch['tag_id'] for ch in ti) for ti in parents))) tagids.update(tag['id'] for tag in tags) tfn = lambda ti: session.getBuildTargets(buildTagID=ti) loaded = bulk_load(session, tfn, tagids) targets = chain(*filter(None, loaded.values())) return list(targets)
def as_taginfo(session: ClientSession, tag: TagSpec) -> TagInfo: """ Coerces a tag value into a koji tag info dict. If tag is an * int, will attempt to load as a tag ID * str, will attempt to load as a tag name * dict, will presume already a tag info :param session: active koji session :param tag: value to lookup :raises NoSuchTag: if the tag value could not be resolved into a tag info dict :since: 1.0 """ if isinstance(tag, (str, int)): if version_check(session, (1, 23)): info = session.getTag(tag, blocked=True) else: info = session.getTag(tag) elif isinstance(tag, dict): info = tag else: info = None if not info: raise NoSuchTag(tag) return info
def cli_renum_tag(session: ClientSession, tagname: Union[int, str], begin: int = 10, step: int = 10, verbose: bool = False, test: bool = False): as_taginfo(session, tagname) original = session.getInheritanceData(tagname) renumbered = renum_inheritance(original, begin, step) if test or verbose: print("Renumbering inheritance priorities for", tagname) for left, right in zip(original, renumbered): name = left['name'] lp = left['priority'] rp = right['priority'] print(f" {lp:>3} -> {rp:>3} {name}") if test: print("Changes not committed in test mode.") else: session.setInheritanceData(tagname, renumbered)
def gather_hosts_checkins( session: ClientSession, arches: Optional[List[str]] = None, channel: Optional[str] = None, skiplist: Optional[List[str]] = None) -> List[DecoratedHostInfo]: """ Similar to session.listHosts, but results are decorated with a new "last_update" entry, which is the timestamp for the host's most recent check-in with the hub. This can be used to identify builders which are enabled, but no longer responding. :param session: an active koji client session :param arches: List of architecture names to filter builders by. Default, all arches :param channel: Channel name to filter builders by. Default, builders in any channel. :param skiplist: List of glob-style patterns of builders to omit. Default, all builders included :since: 1.0 """ arches = arches or None # listHosts only accepts channel filtering by the ID, so let's # resolve those. This should also work if channel is already an # ID, and should validate that the channel exists. if channel: chan_data = session.getChannel(channel) if chan_data is None: raise NoSuchChannel(channel) chan_id = chan_data["id"] else: chan_id = None loaded: Iterable[HostInfo] loaded = session.listHosts(arches, chan_id, None, True, None, None) loaded = filter(None, loaded) if skiplist: loaded = globfilter(loaded, skiplist, key="name", invert=True) # collect a mapping of builder ids to builder info bldrs: Dict[int, DecoratedHostInfo] bldrs = {b["id"]: cast(DecoratedHostInfo, b) for b in loaded} updates = iter_bulk_load(session, session.getLastHostUpdate, bldrs) # correlate the update timestamps with the builder info for bldr_id, data in updates: data = parse_datetime(data, strict=False) if data else None bldrs[bldr_id]["last_update"] = data return list(bldrs.values())
def as_userinfo(session: ClientSession, user: UserSpec) -> UserInfo: """ Resolves user to a userinfo dict. If user is a str or int, then getUser will be invoked. If user is already a dict, it's presumed to be a userinfo already and it's returned unaltered. :param session: active koji session :param user: Name, ID, or User Info describing a koji user :raises NoSuchUser: when user cannot be found :since: 1.0 """ if isinstance(user, (str, int)): session_vars = vars(session) new_get_user = session_vars.get("__new_get_user") if new_get_user: # we've tried the new way and it worked, so keep doing it. info = session.getUser(user, False, True) elif new_get_user is None: # an API incompatibility emerged at some point in Koji's # past, so we need to try the new way first and fall back # to the older signature if that fails. This happened # before Koji hub started reporting its version, so we # cannot use the version_check function to gate this. try: info = session.getUser(user, False, True) session_vars["__new_get_user"] = True except ParameterError: info = session.getUser(user) session_vars["__new_get_user"] = False else: # we've already tried the new way once and it didn't work. info = session.getUser(user) elif isinstance(user, dict): info = user else: info = None if not info: raise NoSuchUser(user) return info
def iter_bulk_load(session: ClientSession, loadfn: Callable[[Any], Any], keys: Iterable[KT], err: bool = True, size: int = 100) -> Iterator[Tuple[KT, Any]]: """ Generic bulk loading generator. Invokes the given loadfn on each key in keys using chunking multicalls limited to the specified size. Yields (key, result) pairs in order. If err is True (default) then any faults will raise an exception. If err is False, then a None will be substituted as the result for the failing key. :param session: The koji session :param loadfn: The loading function, to be invoked in a multicall arrangement. Will be called once with each given key from keys :param keys: The sequence of keys to be used to invoke loadfn. :param err: Whether to raise any underlying fault returns as exceptions. Default, True :param size: How many calls to loadfn to chunk up for each multicall. Default, 100 :raises koji.GenericError: if err is True and an issue occurrs while invoking the loadfn :since: 1.0 """ for key_chunk in chunkseq(keys, size): session.multicall = True for key in key_chunk: loadfn(key) for key, info in zip(key_chunk, session.multiCall(strict=err)): if info: if "faultCode" in info: if err: raise convertFault(Fault(**info)) # type: ignore else: yield key, None else: yield key, info[0] # type: ignore else: yield key, None
def as_taskinfo(session: ClientSession, task: TaskSpec) -> TaskInfo: """ Coerces a task value into a koji task info dict. If task is an * int, will attempt to load as a task ID * dict, will presume already a task info Note that if this function does attempt to load a task, it will request it with the task's request data as well. :param session: active koji session :param task: value to lookup :raises NoSuchTask: if the task value could not be resolved into a task info dict :since: 1.0 """ if isinstance(task, int): info = session.getTaskInfo(task, True) elif isinstance(task, dict): info = task else: info = None if not info: raise NoSuchTask(task) return info
def as_packageinfo(session: ClientSession, pkg: PackageSpec) -> PackageInfo: """ Coerces a host value into a host info dict. If pkg is an: * int, will attempt to load as a package ID * str, will attempt to load as a package name * dict, will presume already a package info :param session: an active koji client session :param pkg: value to lookup :raises NoSuchPackage: if the pkg value could not be resolved into a package info dict :since: 1.1 """ if isinstance(pkg, (str, int)): info = session.getPackage(pkg) elif isinstance(pkg, dict): info = pkg else: info = None if not info: raise NoSuchPackage(pkg) return info
def as_hostinfo(session: ClientSession, host: HostSpec) -> HostInfo: """ Coerces a host value into a host info dict. If host is an: * int, will attempt to load as a host ID * str, will attempt to load as a host name * dict, will presume already a host info :param session: active koji session :param host: value to lookup :raises NoSuchHost: if the host value could not be resolved into a host info dict :since: 1.0 """ if isinstance(host, (str, int)): info = session.getHost(host) elif isinstance(host, dict): info = host else: info = None if not info: raise NoSuchHost(host) return info
def as_targetinfo(session: ClientSession, target: TargetSpec) -> TargetInfo: """ Coerces a target value into a koji target info dict. If target is an * int, will attempt to load as a target ID * str, will attempt to load as a target name * dict, will presume already a target info :param session: active koji session :param target: value to lookup :raises NoSuchTarget: if the target value could not be resolved into a target info dict :since: 1.0 """ if isinstance(target, (str, int)): info = session.getBuildTarget(target) elif isinstance(target, dict): info = target else: info = None if not info: raise NoSuchTarget(target) return info
def as_channelinfo(session: ClientSession, channel: ChannelSpec) -> ChannelInfo: """ Coerces a channel value into a koji channel info dict. If channel is an * int, will attempt to load as a channel ID * str, will attempt to load as a channel name * dict, will presume already a channel info :param session: an active koji client session :param channel: value to lookup :raises NoSuchChannel: if the channel value could not be resolved into a channel info dict :since: 1.1 """ if isinstance(channel, (str, int)): info = session.getChannel(channel) elif isinstance(channel, dict): info = channel else: info = None if not info: raise NoSuchChannel(channel) return info
def as_buildinfo(session: ClientSession, build: BuildSpec) -> BuildInfo: """ Coerces a build value into a koji build info dict. If build is an * int, will attempt to load as a build ID * str, will attempt to load as an NVR * dict, will presume already a build info :param session: active koji session :param build: value to lookup :raises NoSuchBuild: if the build value could not be resolved into a build info dict :since: 1.0 """ if isinstance(build, (str, int)): info = session.getBuild(build) elif isinstance(build, dict): info = build else: info = None if not info: raise NoSuchBuild(build) return info
def as_rpminfo(session: ClientSession, rpm: RPMSpec) -> RPMInfo: """ Coerces a host value into a RPM info dict. If rpm is specified as an: * int, will attempt to load as a RPM ID * str, will attempt to load as a RPM NVRA * dict, will presume already an RPM info :param session: active koji session :param rpm: value to lookup :raises NoSuchRPM: if the rpm value could not be resolved into a RPM info dict :since: 1.0 """ info: RPMInfo if isinstance(rpm, (str, int)): info = session.getRPM(rpm) # type: ignore elif isinstance(rpm, dict): info = rpm else: info = None if not info: raise NoSuchRPM(rpm) return info
def bulk_list_packages( self, session: ClientSession, tag_ids: Iterable[int], inherited: bool = True) -> Dict[int, List[TagPackageInfo]]: """ a multicall caching wrapper for ``session.listPackages`` shares the same cache as `list_packages` (and therefore `allowed_packages` and `blocked_packages`) """ cache = cast(Dict[Tuple[int, bool], List[TagPackageInfo]], self._mixin_cache("list_packages")) result: Dict[int, List[TagPackageInfo]] = {} needed = [] for tid in tag_ids: if (tid, inherited) not in cache: needed.append(tid) else: result[tid] = cache[(tid, inherited)] fn = lambda i: session.listPackages(i, inherited=inherited) for tid, pkgs in iter_bulk_load(session, fn, needed): result[tid] = cache[(tid, inherited)] = pkgs return result
def list_archives_by_builds( build_ids: List[int], build_type: str, session: koji.ClientSession) -> List[Optional[List[Dict]]]: """ Retrieve information about archives by builds :param build_ids: List of build IDs :param build_type: build type, such as "image" :param session: instance of Brew session :return: a list of Koji/Brew archive lists (augmented with "rpms" entries for RPM lists) """ tasks = [] with session.multicall(strict=True) as m: for build_id in build_ids: if not build_id: tasks.append(None) continue tasks.append(m.listArchives(buildID=build_id, type=build_type)) archives_list = [task.result if task else None for task in tasks] # each archives record contains an archive per arch; look up RPMs for each archives = [ar for rec in archives_list for ar in rec or []] archives_rpms = list_image_rpms([ar["id"] for ar in archives], session) for archive, rpms in zip(archives, archives_rpms): archive["rpms"] = rpms return archives_list
def gather_latest_maven_archives( session: ClientSession, tagname: TagSpec, inherit: bool = True, path: Optional[PathSpec] = None) -> List[DecoratedArchiveInfo]: """ Similar to session.getLatestMavenArchives(tagname) but augments the results to include a new "filepath" entry which will point to the matching maven artifact's file location. :param session: an active koji client session :param tagname: Name of the tag to search in for maven artifacts :param inherit: Follow tag inheritance, default True :raises NoSuchTag: if specified tag doesn't exist """ tag = as_taginfo(session, tagname) path = as_pathinfo(path) found = session.getLatestMavenArchives(tag['id'], inherit=inherit) for f in found: # unlike getLatestRPMs, getLatestMavenArchives only provides # the archives themselves. We don't want to have to do a bulk # load for all those, so we fake a build info from the values # in the archive itself. Since we're only using it to # determine paths, the missing fields shouldn't be a problem bld = _fake_maven_build(f, path) d = cast(DecoratedArchiveInfo, f) d["filepath"] = join(bld["build_path"], path.mavenfile(f)) return cast(List[DecoratedArchiveInfo], found)
def get_tagged_builds(tag_component_tuples: Iterable[Tuple[str, Optional[str]]], build_type: Optional[str], event: Optional[int], session: koji.ClientSession, inherit: bool = False) -> List[Optional[List[Dict]]]: """ Get tagged builds as of the given event In each list for a component, builds are ordered from newest tagged to oldest tagged: https://pagure.io/koji/blob/3fed02c8adb93cde614af9f61abd12bbccdd6682/f/hub/kojihub.py#_1392 :param tag_component_tuples: List of (tag, component_name) tuples :param build_type: if given, only retrieve specified build type (rpm, image) :param event: Brew event ID, or None for now. :param session: instance of Brew session :param inherit: True to include builds inherited from parent tags :return: a list of lists of Koji/Brew build dicts """ tasks = [] with session.multicall(strict=True) as m: for tag, component_name in tag_component_tuples: if not tag: tasks.append(None) continue tasks.append( m.listTagged(tag, event=event, package=component_name, type=build_type, inherit=inherit)) return [task.result if task else None for task in tasks]
def collect_cg_access(session: ClientSession, user: UserSpec) -> List[NamedCGInfo]: """ List of content generators user has access to run CGImport with. :param session: an active koji client session :param user: Name, ID, or userinfo dict :raises NoSuchUser: if user is an ID or name which cannot be resolved :since: 1.0 """ userinfo = as_userinfo(session, user) username = userinfo["name"] found = [] for cgname, val in session.listCGs().items(): if username in val.get("users", ()): nval = cast(NamedCGInfo, val) nval["name"] = cgname found.append(nval) return found
def gather_build_image_archives( session: ClientSession, binfo: BuildSpec, path: Optional[PathSpec] = None) -> List[DecoratedArchiveInfo]: """ Gathers a list of image archives for a given build_info. The archive records are augmented with an additional "filepath" entry, the value of which is an expanded path to the file itself. :param session: an active koji client session :param binfo: Build info to fetch archives for :param path: The root dir for the archive file paths, default None :raises NoSuchBuild: if binfo could not be resolved """ binfo = as_buildinfo(session, binfo) bid = binfo["id"] path = as_pathinfo(path) build_path = path.imagebuild(binfo) found = session.listArchives(buildID=bid, type="image") for f in found: d = cast(DecoratedArchiveInfo, f) d["filepath"] = join(build_path, f["filename"]) return cast(List[DecoratedArchiveInfo], found)
def collect_cgs(session: ClientSession, name: Optional[str] = None) -> List[NamedCGInfo]: """ :param name: only collect the given CG. Default, collect all :raises NoSuchContentGenerator: if name is specified and no content generator matches :since: 1.0 """ cgs = session.listCGs() if name: # filter the cgs dict down to just the named one if name in cgs: cgs = {name: cgs[name]} else: raise NoSuchContentGenerator(name) result = [] # convert the cgs dict into a list, augmenting the cg data with # its own name for name, cg in cgs.items(): ncg = cast(NamedCGInfo, cg) ncg["name"] = name result.append(ncg) return result
def iter_bulk_tag_builds( session: ClientSession, tag: TagSpec, build_infos: BuildInfos, force: bool = False, notify: bool = False, size: int = 100, strict: bool = False) -> Iterator[List[Tuple[BuildInfo, Any]]]: """ Tags a large number of builds using multicall invocations of tagBuildBypass. Builds are specified by build info dicts. yields lists of tuples containing a build info dict and the result of the tagBuildBypass call for that build. This gives the caller a chance to record the results of each multicall, and to present feedback to a user to indicate that the operations are continuing. :param session: an active koji session :param tag: Destination tag's name or ID :param build_infos: Build infos to be tagged :param force: Force tagging. Re-tags if necessary, bypasses policy. Default, False :param notify: Send tagging notifications. Default, False :param size: Count of tagging operations to perform in a single multicall. Default, 100 :param strict: Raise an exception and discontinue execution at the first error. Default, False :raises NoSuchTag: If tag does not exist """ tag = as_taginfo(session, tag) tagid = tag["id"] for build_chunk in chunkseq(build_infos, size): session.multicall = True for build in build_chunk: session.tagBuildBypass(tagid, build["id"], force, notify) results = session.multiCall(strict=strict) yield list(zip(build_chunk, results))
def collect_userinfo(session: ClientSession, user: UserSpec) -> DecoratedUserInfo: """ Gather information about a named user, including the list of permissions the user has. Will convert the older `'krb_principal'` value (koji < 1.19) into a `'krb_principals'` list (koji >= 1.19) to provide some level of uniformity. :param session: an active koji client session :param user: name of a user or their kerberos ID :raises NoSuchUser: if user is an ID or name which cannot be resolved :since: 1.0 """ userinfo = cast(DecoratedUserInfo, as_userinfo(session, user)) # depending on koji version, getUser resulted in either a # krb_principal or krb_principals entry (or neither if it's not # set up for kerberos). Let's normalize on the newer # krb_principals one by converting. See # https://pagure.io/koji/issue/1629 if "krb_principal" in userinfo: krb = userinfo["krb_principal"] userinfo["krb_principals"] = [krb] if krb else [] uid = userinfo["id"] userinfo["permissions"] = session.getUserPerms(uid) userinfo["content_generators"] = collect_cg_access(session, userinfo) if userinfo.get("usertype", UserType.NORMAL) == UserType.GROUP: try: userinfo["members"] = session.getGroupMembers(uid) except Exception: # non-admin accounts cannot query group membership, so omit userinfo["members"] = None # type: ignore return userinfo
def list_image_rpms(image_ids: List[int], session: koji.ClientSession) -> List[Optional[List[Dict]]]: """ Retrieve RPMs in given images :param image_ids: image IDs list :param session: instance of Brew session :return: a list of Koji/Brew RPM lists """ with session.multicall(strict=True) as m: tasks = [m.listRPMs(imageID=image_id) for image_id in image_ids] return [task.result for task in tasks]
def untag_builds(tag: str, builds: List[str], session: koji.ClientSession): tasks = [] with session.multicall(strict=False) as m: for build in builds: if not build: tasks.append(None) continue tasks.append(m.untagBuild(tag, build)) return tasks
def ensure_tag(session: ClientSession, name: str) -> TagInfo: """ Given a name, resolve it to a tag info dict. If there is no such tag, then create it and return its newly created tag info. :param session: active koji session :param name: tag name :since: 1.0 """ try: session.createTag(name) except GenericError: pass return as_taginfo(session, name)
def list_build_rpms(build_ids: List[int], session: koji.ClientSession) -> List[Optional[List[Dict]]]: """ Retrieve RPMs in given package builds (not images) :param build_ids: list of build IDs :param session: instance of Brew session :return: a list of Koji/Brew RPM lists """ with session.multicall(strict=True) as m: tasks = [m.listBuildRPMs(build) for build in build_ids] return [task.result for task in tasks]
REPO_DIR = '/var/cache/fedoracommunity/git.fedoraproject.org' TIMESTAMP = '/var/cache/fedoracommunity/git.fedoraproject.org/.timestamp' # Get a list of active git branches pkgdb = PackageDB() collections = pkgdb.get_collection_list(eol=False) active = [c[0].gitbranchname for c in collections] # Grab a list of all koji builds since our last run # if there is no saved timestamp, do a full run packages = [] if not os.path.exists(TIMESTAMP): packages = os.listdir(REPO_DIR) else: timestamp = file(TIMESTAMP).read().strip() koji = ClientSession('http://koji.fedoraproject.org/kojihub') builds = koji.listBuilds(createdAfter=float(timestamp)) packages = set([build['name'] for build in builds]) packages = [pkg for pkg in packages if os.path.isdir(os.path.join(REPO_DIR, pkg))] for repo in packages: print("[ %s ]" % repo) for branch in os.listdir(os.path.join(REPO_DIR, repo)): if branch in active: subprocess.call('git pull', shell=True, cwd=os.path.join(REPO_DIR, repo, branch)) else: print("Deleting EOL branch %s" % branch) shutil.rmtree(os.path.join(REPO_DIR, repo, branch))