1
0
mirror of https://github.com/openSUSE/osc.git synced 2024-11-15 08:36:13 +01:00
github.com_openSUSE_osc/osc/obs_scm/serviceinfo.py

254 lines
9.9 KiB
Python

import hashlib
import os
import shutil
import tempfile
import time
from typing import Optional
from urllib.error import HTTPError
from urllib.parse import urlparse
from .. import oscerr
from .. import output
from ..util.xml import ET
class Serviceinfo:
"""Source service content
"""
def __init__(self):
"""creates an empty serviceinfo instance"""
self.services = []
self.apiurl: Optional[str] = None
self.project: Optional[str] = None
self.package: Optional[str] = None
def read(self, serviceinfo_node, append=False):
"""read in the source services ``<services>`` element passed as
elementtree node.
"""
def error(msg, xml):
from ..core import ET_ENCODING
data = f'invalid service format:\n{ET.tostring(xml, encoding=ET_ENCODING)}'
raise ValueError(f"{data}\n\n{msg}")
if serviceinfo_node is None:
return
if not append:
self.services = []
services = serviceinfo_node.findall('service')
for service in services:
name = service.get('name')
if name is None:
error("invalid service definition. Attribute name missing.", service)
if len(name) < 3 or '/' in name:
error(f"invalid service name: {name}", service)
mode = service.get('mode', '')
data = {'name': name, 'mode': mode}
command = [name]
for param in service.findall('param'):
option = param.get('name')
if option is None:
error(f"{name}: a parameter requires a name", service)
value = ''
if param.text:
value = param.text
command.append('--' + option)
# hmm is this reasonable or do we want to allow real
# options (e.g., "--force" (without an argument)) as well?
command.append(value)
data['command'] = command
self.services.append(data)
def getProjectGlobalServices(self, apiurl: str, project: str, package: str):
from ..core import http_POST
from ..core import makeurl
self.apiurl = apiurl
# get all project wide services in one file, we don't store it yet
u = makeurl(apiurl, ["source", project, package], query={"cmd": "getprojectservices"})
try:
f = http_POST(u)
root = ET.parse(f).getroot()
self.read(root, True)
self.project = project
self.package = package
except HTTPError as e:
if e.code == 404 and package != '_project':
self.getProjectGlobalServices(apiurl, project, '_project')
self.package = package
elif e.code != 403 and e.code != 400:
raise e
def addVerifyFile(self, serviceinfo_node, filename: str):
f = open(filename, 'rb')
digest = hashlib.sha256(f.read()).hexdigest()
f.close()
r = serviceinfo_node
s = ET.Element("service", name="verify_file")
ET.SubElement(s, "param", name="file").text = filename
ET.SubElement(s, "param", name="verifier").text = "sha256"
ET.SubElement(s, "param", name="checksum").text = digest
r.append(s)
return r
def addDownloadUrl(self, serviceinfo_node, url_string: str):
url = urlparse(url_string)
protocol = url.scheme
host = url.netloc
path = url.path
r = serviceinfo_node
s = ET.Element("service", name="download_url")
ET.SubElement(s, "param", name="protocol").text = protocol
ET.SubElement(s, "param", name="host").text = host
ET.SubElement(s, "param", name="path").text = path
r.append(s)
return r
def addSetVersion(self, serviceinfo_node):
r = serviceinfo_node
s = ET.Element("service", name="set_version", mode="buildtime")
r.append(s)
return r
def addGitUrl(self, serviceinfo_node, url_string: Optional[str]):
r = serviceinfo_node
s = ET.Element("service", name="obs_scm")
ET.SubElement(s, "param", name="url").text = url_string
ET.SubElement(s, "param", name="scm").text = "git"
r.append(s)
return r
def addTarUp(self, serviceinfo_node):
r = serviceinfo_node
s = ET.Element("service", name="tar", mode="buildtime")
r.append(s)
return r
def addRecompressTar(self, serviceinfo_node):
r = serviceinfo_node
s = ET.Element("service", name="recompress", mode="buildtime")
ET.SubElement(s, "param", name="file").text = "*.tar"
ET.SubElement(s, "param", name="compression").text = "xz"
r.append(s)
return r
def execute(self, dir, callmode: Optional[str] = None, singleservice=None, verbose: Optional[bool] = None):
old_dir = os.path.join(dir, '.old')
# if 2 osc instances are executed at a time one, of them fails on .old file existence
# sleep up to 10 seconds until we can create the directory
for i in reversed(range(10)):
try:
os.mkdir(old_dir)
break
except FileExistsError:
time.sleep(1)
if i == 0:
msg = f'"{old_dir}" exists, please remove it'
raise oscerr.OscIOError(None, msg)
try:
result = self._execute(dir, old_dir, callmode, singleservice, verbose)
finally:
shutil.rmtree(old_dir)
return result
def _execute(
self, dir, old_dir, callmode: Optional[str] = None, singleservice=None, verbose: Optional[bool] = None
):
from ..core import get_osc_version
from ..core import run_external
from ..core import vc_export_env
# cleanup existing generated files
for filename in os.listdir(dir):
if filename.startswith('_service:') or filename.startswith('_service_'):
os.rename(os.path.join(dir, filename),
os.path.join(old_dir, filename))
allservices = self.services or []
service_names = [s['name'] for s in allservices]
if singleservice and singleservice not in service_names:
# set array to the manual specified singleservice, if it is not part of _service file
data = {'name': singleservice, 'command': [singleservice], 'mode': callmode}
allservices = [data]
elif singleservice:
allservices = [s for s in allservices if s['name'] == singleservice]
# set the right called mode or the service would be skipped below
for s in allservices:
s['mode'] = callmode
if not allservices:
# short-circuit to avoid a potential http request in vc_export_env
# (if there are no services to execute this http request is
# useless)
return 0
# services can detect that they run via osc this way
os.putenv("OSC_VERSION", get_osc_version())
# set environment when using OBS 2.3 or later
if self.project is not None:
# These need to be kept in sync with bs_service
os.putenv("OBS_SERVICE_APIURL", self.apiurl)
os.putenv("OBS_SERVICE_PROJECT", self.project)
os.putenv("OBS_SERVICE_PACKAGE", self.package)
# also export vc env vars (some services (like obs_scm) use them)
vc_export_env(self.apiurl)
# recreate files
ret = 0
for service in allservices:
if callmode != "all":
if service['mode'] == "buildtime":
continue
if service['mode'] == "serveronly" and callmode != "local":
continue
if service['mode'] == "manual" and callmode != "manual":
continue
if service['mode'] != "manual" and callmode == "manual":
continue
if service['mode'] == "disabled" and callmode != "disabled":
continue
if service['mode'] != "disabled" and callmode == "disabled":
continue
if service['mode'] != "trylocal" and service['mode'] != "localonly" and callmode == "trylocal":
continue
temp_dir = None
try:
temp_dir = tempfile.mkdtemp(dir=dir, suffix=f".{service['name']}.service")
cmd = service['command']
if not os.path.exists("/usr/lib/obs/service/" + cmd[0]):
raise oscerr.PackageNotInstalled(f"obs-service-{cmd[0]}")
cmd[0] = "/usr/lib/obs/service/" + cmd[0]
cmd = cmd + ["--outdir", temp_dir]
output.print_msg(f"Running source_service '{service['name']}' ...", print_to="stdout")
output.print_msg("Run source service:", " ".join(cmd), print_to="verbose")
r = run_external(*cmd)
if r != 0:
print("Aborting: service call failed: ", ' '.join(cmd))
# FIXME: addDownloadUrlService calls si.execute after
# updating _services.
return r
if service['mode'] == "manual" or service['mode'] == "disabled" or service['mode'] == "trylocal" or service['mode'] == "localonly" or callmode == "local" or callmode == "trylocal" or callmode == "all":
for filename in os.listdir(temp_dir):
os.rename(os.path.join(temp_dir, filename), os.path.join(dir, filename))
else:
name = service['name']
for filename in os.listdir(temp_dir):
os.rename(os.path.join(temp_dir, filename), os.path.join(dir, "_service:" + name + ":" + filename))
finally:
if temp_dir is not None:
shutil.rmtree(temp_dir)
return 0