]>
git.ipfire.org Git - pbs.git/blob - src/buildservice/__init__.py
11 import systemd
.journal
16 from . import bugtracker
17 from . import builders
21 from . import database
22 from . import distribution
24 from . import httpclient
27 from . import logstreams
28 from . import messages
30 from . import packages
31 from . import releasemonitoring
32 from . import repository
33 from . import settings
34 from . import sessions
40 log
= logging
.getLogger("pbs")
43 from .__version
__ import VERSION
as __version__
45 from .decorators
import *
46 from .constants
import *
48 class Backend(object):
51 # A list of any background tasks
54 def __init__(self
, config_file
, test
=False):
57 # Read configuration file
58 self
.config
= self
.read_config(config_file
)
61 self
.basepath
= self
.config
.get("global", "basepath")
63 # Global pakfire settings (from database).
64 self
.settings
= settings
.Settings(self
)
66 # Initialize the HTTP Client
67 self
.httpclient
= httpclient
.HTTPClient(self
)
69 self
.aws
= aws
.AWS(self
)
70 self
.builds
= builds
.Builds(self
)
71 self
.cache
= cache
.Cache(self
)
72 self
.jobs
= jobs
.Jobs(self
)
73 self
.builders
= builders
.Builders(self
)
74 self
.distros
= distribution
.Distributions(self
)
75 self
.events
= events
.Events(self
)
76 self
.keys
= keys
.Keys(self
)
77 self
.logstreams
= logstreams
.LogStreams(self
)
78 self
.messages
= messages
.Messages(self
)
79 self
.mirrors
= mirrors
.Mirrors(self
)
80 self
.packages
= packages
.Packages(self
)
81 self
.releasemonitoring
= releasemonitoring
.ReleaseMonitoring(self
)
82 self
.repos
= repository
.Repositories(self
)
83 self
.sessions
= sessions
.Sessions(self
)
84 self
.sources
= sources
.Sources(self
)
85 self
.uploads
= uploads
.Uploads(self
)
86 self
.users
= users
.Users(self
)
88 # Open a connection to bugzilla.
89 self
.bugzilla
= bugtracker
.Bugzilla(self
)
91 # Create a temporary directory
92 self
._create
_tmp
_path
()
94 log
.info("Pakfire Build Service initialized at %s" % self
.basepath
)
96 def read_config(self
, path
):
97 c
= configparser
.ConfigParser()
99 # Read configuration from file
108 name
= self
.config
.get("database", "name")
109 hostname
= self
.config
.get("database", "hostname")
110 user
= self
.config
.get("database", "user")
111 password
= self
.config
.get("database", "password")
112 except configparser
.Error
as e
:
113 log
.error("Error parsing the config: %s" % e
.message
)
115 log
.debug("Connecting to database %s @ %s" % (name
, hostname
))
117 return database
.Connection(hostname
, name
, user
=user
, password
=password
)
119 def _create_tmp_path(self
):
121 This function will create some temporary space with the correct permissions.
123 path
= self
.path("tmp")
126 os
.mkdir(path
, mode
=0o1777)
128 # Ignore if the directory already exists
129 except FileExistsError
:
132 def path(self
, *args
):
134 Takes a relative path and makes it absolute
136 return os
.path
.join(self
.basepath
, *args
)
138 def url_to(self
, url
):
140 Takes a relative URL and makes it absolute
143 baseurl
= self
.settings
.get("baseurl")
145 # Join it all together
146 return urllib
.parse
.urljoin(baseurl
, url
)
148 def path_to_url(self
, path
):
150 Takes a path to a file on the file system and converts it into a URL
154 "files", os
.path
.relpath(path
, self
.basepath
),
157 return self
.url_to(path
)
159 def pakfire(self
, *args
, **kwargs
):
161 Launches a new Pakfire instance with the given configuration
163 return config
.PakfireConfig(self
, *args
, **kwargs
)
165 # Functions to run something in background
167 def run_task(self
, callback
, *args
):
169 Runs the given coroutine in the background
172 task
= asyncio
.create_task(callback(*args
))
174 # Keep a reference to the task and remove it when the task has finished
175 self
.__tasks
.add(task
)
176 task
.add_done_callback(self
.__tasks
.discard
)
180 def run_periodic_task(self
, delay
, callback
, *args
):
182 Calls the given callback periodically in the background
184 self
.run_task(self
._periodic
_task
, delay
, callback
, *args
)
186 async def _periodic_task(self
, delay
, callback
, *args
):
188 Helper function for run_periodic_task() that will call the given
189 callback regulary after the timer has expired.
191 log
.debug("Periodic callback %r started" % callback
)
194 # Wait a little moment
195 await asyncio
.sleep(delay
)
198 ret
= callback(*args
)
200 # Await ret if callback is a coroutine
201 if inspect
.isawaitable(ret
):
204 except Exception as e
:
205 log
.error("Exception in periodic callback %r" % callback
, exc_info
=True)
209 async def command(self
, *command
, krb5_auth
=False, **kwargs
):
211 Runs this shell command
213 with tempfile
.TemporaryDirectory() as tmp
:
214 # Create a minimal environment
216 "HOME" : os
.environ
.get("HOME", tmp
),
218 # Tell the system where to put temporary files
221 # Store any Kerberos credentials here
222 "KRB5CCNAME" : os
.path
.join(tmp
, ".krb5cc"),
225 # Authenticate using Kerberos
227 await self
._krb
5_auth
(env
=env
)
230 return await self
._command
(*command
, env
=env
, **kwargs
)
232 async def _command(self
, *command
, return_output
=False, **kwargs
):
233 log
.debug("Running command: %s" % " ".join(command
))
236 process
= await asyncio
.create_subprocess_exec(
238 stdin
=asyncio
.subprocess
.DEVNULL
,
239 stdout
=asyncio
.subprocess
.PIPE
,
240 stderr
=asyncio
.subprocess
.PIPE
,
246 # Fetch output of command and send it to the logger
248 line
= await process
.stdout
.readline()
261 # Store the output if requested
265 # Wait until the process has finished
266 returncode
= await process
.wait()
268 # Check the return code
270 # Fetch any output from the standard error output
271 stderr
= await process
.stderr
.read()
272 stderr
= stderr
.decode()
275 log
.error("Error running command: %s (code=%s)" % (
276 " ".join(command
), returncode
,
281 raise CommandExecutionError(returncode
, stderr
)
283 # Return output if requested
285 return "\n".join(stdout
)
287 async def _krb5_auth(self
, **kwargs
):
288 log
.debug("Performing Kerberos authentication...")
290 # Fetch path to keytab
291 keytab
= self
.settings
.get("krb5-keytab")
293 log
.warning("No keytab configured")
296 # Fetch Kerberos principal
297 principal
= self
.settings
.get("krb5-principal")
299 log
.warning("No Kerberos principal configured")
302 # Fetch a Kerberos ticket
304 "kinit", "-k", "-t", keytab
, principal
, **kwargs
,
307 async def copy(self
, src
, dst
, mode
=None):
309 Copies a file from src to dst
311 log
.debug("Copying %s to %s" % (src
, dst
))
313 # Create parent directory
314 await self
.make_parent_directory(dst
)
316 # Copy data without any metadata
317 await asyncio
.to_thread(shutil
.copyfile
, src
, dst
)
321 await asyncio
.to_thread(os
.chmod
, dst
, mode
)
323 async def make_parent_directory(self
, path
):
325 Creates the parent directory of path
327 path
= os
.path
.dirname(path
)
329 # Create destination path (if it does not exist)
331 await asyncio
.to_thread(os
.makedirs
, path
)
332 except FileExistsError
:
335 async def unlink(self
, path
):
340 path
= os
.path
.abspath(path
)
342 # Check if the path is within our base directory
343 if not path
.startswith(self
.basepath
):
344 raise OSError("Cannot delete %s which is outside %s" % (path
, self
.basepath
))
346 log
.debug("Unlinking %s" % path
)
348 await asyncio
.to_thread(self
._unlink
, path
)
350 def _unlink(self
, path
):
351 # Unlink the file we were asked to unlink
357 # Try to delete any empty parent directories
359 # Get the parent directory
360 path
= os
.path
.dirname(path
)
362 # Break if we reached the base path
363 if path
== self
.basepath
:
372 log
.debug(" Cleaned up %s..." % path
)
374 def tempfile(self
, mode
="w+b", delete
=True):
376 Returns an open file handle to a new temporary file
378 path
= self
.path("tmp")
380 return tempfile
.NamedTemporaryFile(mode
=mode
, dir=path
, delete
=delete
)
382 def _write_tempfile(self
, content
):
384 Writes the content to a temporary file and returns its path
386 t
= self
.tempfile(delete
=False)
390 t
.write(content
.encode())
397 async def open(self
, path
):
399 Opens a package and returns the archive
401 return await asyncio
.to_thread(self
._open
, path
)
403 def _open(self
, path
):
404 log
.debug("Opening %s..." % path
)
407 with self
.pakfire() as p
:
411 def ssl_context(self
):
413 context
= ssl
.create_default_context()
415 # Fetch client certificate
416 certificate
= self
.settings
.get("client-certificate", None)
417 key
= self
.settings
.get("client-key", None)
419 # Apply client certificate
420 if certificate
and key
:
421 with tempfile
.NamedTemporaryFile(mode
="w") as f_cert
:
422 f_cert
.write(certificate
)
425 with tempfile
.NamedTemporaryFile(mode
="w") as f_key
:
429 context
.load_cert_chain(f_cert
.name
, f_key
.name
)
433 async def load_certificate(self
, certfile
, keyfile
):
434 with self
.db
.transaction():
436 with
open(certfile
) as f
:
437 self
.settings
.set("client-certificate", f
.read())
440 with
open(keyfile
) as f
:
441 self
.settings
.set("client-key", f
.read())
443 log
.info("Updated certificates")
445 async def cleanup(self
):
447 Called regularly to cleanup any left-over resources
450 await self
.messages
.queue
.cleanup()
453 await self
.sessions
.cleanup()
456 await self
.uploads
.cleanup()
458 async def sync(self
):
460 Syncs any repository that should be mirrored
462 log
.info("Syncing mirrors...")
464 # Fetch the sync target
465 target
= self
.settings
.get("sync-target")
467 log
.warning("No sync target configured")
473 # Show what is being transferred
476 # Compress any transferred data
479 # Enable archive mode
482 # Preserve hardlinks, ACLs & XATTRs
487 # Delete any files that we have deleted
491 # Remove any empty directories
492 "--prune-empty-dirs",
494 # Make the transaction atomic
497 # Add source & target
498 "%s/" % self
.basepath
,
502 # Add all mirrored repositories
503 for repo
in self
.repos
.mirrored
:
504 path
= os
.path
.relpath(repo
.local_path(), self
.basepath
)
506 commandline
.append("--include=%s***" % path
)
508 # Exclude everything that hasn't been included
509 commandline
+= ("--include=*/", "--exclude=*")
512 await self
.command(*commandline
, krb5_auth
=True)
517 Configures the logger for the buildservice backend
519 # Log everything to journal
520 handler
= systemd
.journal
.JournalHandler(
521 SYSLOG_IDENTIFIER
="pakfire-build-service",
523 log
.addHandler(handler
)