]>
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 jobqueue
27 from . import messages
29 from . import packages
30 from . import repository
31 from . import settings
32 from . import sessions
37 log
= logging
.getLogger("pakfire.buildservice")
40 from .__version
__ import VERSION
as __version__
42 from .decorators
import *
43 from .constants
import *
45 class Backend(object):
48 # A list of any background tasks
51 def __init__(self
, config_file
, test
=False):
54 # Read configuration file
55 self
.config
= self
.read_config(config_file
)
58 self
.basepath
= self
.config
.get("global", "basepath")
60 # Global pakfire settings (from database).
61 self
.settings
= settings
.Settings(self
)
63 self
.aws
= aws
.AWS(self
)
64 self
.builds
= builds
.Builds(self
)
65 self
.cache
= cache
.Cache(self
)
66 self
.jobs
= jobs
.Jobs(self
)
67 self
.builders
= builders
.Builders(self
)
68 self
.distros
= distribution
.Distributions(self
)
69 self
.events
= events
.Events(self
)
70 self
.jobqueue
= jobqueue
.JobQueue(self
)
71 self
.keys
= keys
.Keys(self
)
72 self
.messages
= messages
.Messages(self
)
73 self
.mirrors
= mirrors
.Mirrors(self
)
74 self
.packages
= packages
.Packages(self
)
75 self
.repos
= repository
.Repositories(self
)
76 self
.sessions
= sessions
.Sessions(self
)
77 self
.sources
= sources
.Sources(self
)
78 self
.uploads
= uploads
.Uploads(self
)
79 self
.users
= users
.Users(self
)
81 # Open a connection to bugzilla.
82 self
.bugzilla
= bugtracker
.Bugzilla(self
)
84 log
.info("Pakfire Build Service initialized at %s" % self
.basepath
)
86 def read_config(self
, path
):
87 c
= configparser
.ConfigParser()
89 # Read configuration from file
98 name
= self
.config
.get("database", "name")
99 hostname
= self
.config
.get("database", "hostname")
100 user
= self
.config
.get("database", "user")
101 password
= self
.config
.get("database", "password")
102 except configparser
.Error
as e
:
103 log
.error("Error parsing the config: %s" % e
.message
)
105 log
.debug("Connecting to database %s @ %s" % (name
, hostname
))
107 return database
.Connection(hostname
, name
, user
=user
, password
=password
)
109 def path(self
, *args
):
111 Takes a relative path and makes it absolute
113 return os
.path
.join(self
.basepath
, *args
)
115 def path_to_url(self
, path
):
117 Takes a path to a file on the file system and converts it into a URL
120 baseurl
= self
.settings
.get("baseurl")
124 "files", os
.path
.relpath(path
, self
.basepath
),
127 # Join it all together
128 return urllib
.parse
.urljoin(baseurl
, path
)
130 def pakfire(self
, *args
, **kwargs
):
132 Launches a new Pakfire instance with the given configuration
134 return config
.PakfireConfig(self
, *args
, **kwargs
)
136 # Functions to run something in background
138 def run_task(self
, callback
, *args
):
140 Runs the given coroutine in the background
143 task
= asyncio
.create_task(callback(*args
))
145 # Keep a reference to the task and remove it when the task has finished
146 self
.__tasks
.add(task
)
147 task
.add_done_callback(self
.__tasks
.discard
)
151 def run_periodic_task(self
, delay
, callback
, *args
):
153 Calls the given callback periodically in the background
155 self
.run_task(self
._periodic
_task
, delay
, callback
, *args
)
157 async def _periodic_task(self
, delay
, callback
, *args
):
159 Helper function for run_periodic_task() that will call the given
160 callback regulary after the timer has expired.
162 log
.debug("Periodic callback %r started" % callback
)
165 # Wait a little moment
166 await asyncio
.sleep(delay
)
169 ret
= callback(*args
)
171 # Await ret if callback is a coroutine
172 if inspect
.isawaitable(ret
):
175 except Exception as e
:
176 log
.error("Exception in periodic callback %r" % callback
, exc_info
=True)
180 async def command(self
, *args
, krb5_auth
=False, **kwargs
):
182 Runs this shell command
184 # Authenticate using Kerberos
186 await self
.krb5_auth()
188 log
.debug("Running command: %s" % " ".join(args
))
191 process
= await asyncio
.create_subprocess_exec(
193 stdin
=asyncio
.subprocess
.DEVNULL
,
194 stdout
=asyncio
.subprocess
.PIPE
,
195 stderr
=asyncio
.subprocess
.STDOUT
,
199 # Fetch output of command and send it to the logger
201 line
= await process
.stdout
.readline()
213 # Wait until the process has finished
216 async def krb5_auth(self
):
217 log
.debug("Performing Kerberos authentication...")
219 # Fetch path to keytab
220 keytab
= self
.settings
.get("krb5-keytab")
222 log
.warning("No keytab configured")
225 # Fetch Kerberos principal
226 principal
= self
.settings
.get("krb5-principal")
228 log
.warning("No Kerberos principal configured")
231 # Fetch a Kerberos ticket
233 "kinit", "-k", "-t", keytab
, principal
,
236 async def copy(self
, src
, dst
):
238 Copies a file from src to dst
240 log
.debug("Copying %s to %s" % (src
, dst
))
242 # Create parent directory
243 await self
.make_parent_directory(dst
)
245 # Copy data without any metadata
246 await asyncio
.to_thread(shutil
.copyfile
, src
, dst
)
248 async def make_parent_directory(self
, path
):
250 Creates the parent directory of path
252 path
= os
.path
.dirname(path
)
254 # Create destination path (if it does not exist)
256 await asyncio
.to_thread(os
.makedirs
, path
)
257 except FileExistsError
:
260 async def unlink(self
, path
):
265 path
= os
.path
.abspath(path
)
267 # Check if the path is within our base directory
268 if not path
.startswith(self
.basepath
):
269 raise OSError("Cannot delete %s which is outside %s" % (path
, self
.basepath
))
271 log
.debug("Unlinking %s" % path
)
273 await asyncio
.to_thread(self
._unlink
, path
)
275 def _unlink(self
, path
):
276 # Unlink the file we were asked to unlink
282 # Try to delete any empty parent directories
284 # Get the parent directory
285 path
= os
.path
.dirname(path
)
287 # Break if we reached the base path
288 if path
== self
.basepath
:
297 log
.debug(" Cleaned up %s..." % path
)
299 def _write_tempfile(self
, content
):
301 Writes the content to a temporary file and returns its path
303 t
= tempfile
.NamedTemporaryFile(delete
=False)
307 t
.write(content
.encode())
314 async def open(self
, path
):
316 Opens a package and returns the archive
318 return await asyncio
.to_thread(self
._open
, path
)
320 def _open(self
, path
):
321 log
.debug("Opening %s..." % path
)
324 with self
.pakfire() as p
:
328 def ssl_context(self
):
330 context
= ssl
.create_default_context()
332 # Fetch client certificate
333 certificate
= self
.settings
.get("client-certificate", None)
334 key
= self
.settings
.get("client-key", None)
336 # Apply client certificate
337 if certificate
and key
:
338 with tempfile
.NamedTemporaryFile(mode
="w") as f_cert
:
339 f_cert
.write(certificate
)
342 with tempfile
.NamedTemporaryFile(mode
="w") as f_key
:
346 context
.load_cert_chain(f_cert
.name
, f_key
.name
)
350 async def load_certificate(self
, certfile
, keyfile
):
351 with self
.db
.transaction():
353 with
open(certfile
) as f
:
354 self
.settings
.set("client-certificate", f
.read())
357 with
open(keyfile
) as f
:
358 self
.settings
.set("client-key", f
.read())
360 log
.info("Updated certificates")
362 async def cleanup(self
):
364 Called regularly to cleanup any left-over resources
367 await self
.messages
.queue
.cleanup()
370 await self
.sessions
.cleanup()
373 await self
.uploads
.cleanup()
375 async def sync(self
):
377 Syncs any repository that should be mirrored
379 log
.info("Syncing mirrors...")
381 # Fetch the sync target
382 target
= self
.settings
.get("sync-target")
384 log
.warning("No sync target configured")
390 # Show what is being transferred
393 # Compress any transferred data
396 # Enable archive mode
399 # Preserve hardlinks, ACLs & XATTRs
404 # Delete any files that we have deleted
408 # Remove any empty directories
409 "--prune-empty-dirs",
411 # Make the transaction atomic
414 # Add source & target
415 "%s/" % self
.basepath
,
419 # Add all mirrored repositories
420 for repo
in self
.repos
.mirrored
:
421 path
= os
.path
.relpath(repo
.local_path(), self
.basepath
)
423 commandline
.append("--include=%s***" % path
)
425 # Exclude everything that hasn't been included
426 commandline
+= ("--include=*/", "--exclude=*")
429 await self
.command(*commandline
, krb5_auth
=True)
434 Configures the logger for the buildservice backend
436 # Do not propagate anything from the build service up to any Pakfire instances
439 # Enable debug logging
440 log
.setLevel(logging
.DEBUG
)
442 # Log everything to journal
443 handler
= systemd
.journal
.JournalHandler(
444 SYSLOG_IDENTIFIER
="pakfire-build-service",
446 log
.addHandler(handler
)