2
This is some of the code behind 'cobbler sync'.
4
Copyright 2006-2009, Red Hat, Inc
5
Michael DeHaan <mdehaan@redhat.com>
6
John Eckersberg <jeckersb@redhat.com>
8
This program is free software; you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation; either version 2 of the License, or
11
(at your option) any later version.
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
GNU General Public License for more details.
18
You should have received a copy of the GNU General Public License
19
along with this program; if not, write to the Free Software
20
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
33
from utils import popen2
34
from shlex import shlex
38
from cexceptions import *
50
The mandatory cobbler module registration hook.
52
return "manage/import"
55
class ImportFreeBSDManager:
57
def __init__(self,config,logger):
64
self.distros = config.distros()
65
self.profiles = config.profiles()
66
self.systems = config.systems()
67
self.settings = config.settings()
68
self.repos = config.repos()
69
self.templar = templar.Templar(config)
71
# required function for import modules
73
return "import/freebsd"
75
# required function for import modules
76
def check_for_signature(self,path,cli_breed):
78
'etc/freebsd-update.conf',
83
#self.logger.info("scanning %s for a redhat-based distro signature" % path)
84
for signature in signatures:
85
d = os.path.join(path,signature)
87
self.logger.info("Found a freebsd compatible signature: %s" % signature)
88
return (True,signature)
90
if cli_breed and cli_breed in self.get_valid_breeds():
91
self.logger.info("Warning: No distro signature for kernel at %s, using value from command line" % path)
96
# required function for import modules
97
def run(self,pkgdir,name,path,network_root=None,kickstart_file=None,rsync_flags=None,arch=None,breed=None,os_version=None):
99
self.network_root = network_root
100
self.kickstart_file = kickstart_file
101
self.rsync_flags = rsync_flags
104
self.os_version = os_version
109
# some fixups for the XMLRPC interface, which does not use "None"
110
if self.arch == "": self.arch = None
111
if self.kickstart_file == "": self.kickstart_file = None
112
if self.os_version == "": self.os_version = None
113
if self.rsync_flags == "": self.rsync_flags = None
114
if self.network_root == "": self.network_root = None
116
# If no breed was specified on the command line, set it to "freebsd" for this module
117
if self.breed == None:
118
self.breed = "freebsd"
120
# import takes a --kickstart for forcing selection that can't be used in all circumstances
122
if self.kickstart_file and not self.breed:
123
utils.die(self.logger,"Kickstart file can only be specified when a specific breed is selected")
125
if self.os_version and not self.breed:
126
utils.die(self.logger,"OS version can only be specified when a specific breed is selected")
128
if self.breed and self.breed.lower() not in self.get_valid_breeds():
129
utils.die(self.logger,"Supplied import breed is not supported by this module")
131
# now walk the filesystem looking for distributions that match certain patterns
133
self.logger.info("adding distros")
135
# FIXME : search below self.path for isolinux configurations or known directories from TRY_LIST
136
os.path.walk(self.path, self.distro_adder, distros_added)
138
# find out if we can auto-create any repository records from the install tree
140
if self.network_root is None:
141
self.logger.info("associating repos")
142
# FIXME: this automagic is not possible (yet) without mirroring
143
self.repo_finder(distros_added)
145
# find the most appropriate answer files for each profile object
147
self.logger.info("associating answerfiles")
148
self.kickstart_finder(distros_added)
150
# ensure bootloaders are present
151
self.api.pxegen.copy_bootloaders()
155
# required function for import modules
156
def get_valid_arches(self):
157
return ["i386", "ia64", "ppc", "ppc64", "s390", "s390x", "x86_64", "x86",]
159
# required function for import modules
160
def get_valid_breeds(self):
163
# required function for import modules
164
def get_valid_os_versions(self):
165
return codes.VALID_OS_VERSIONS["freebsd"]
167
def get_valid_repo_breeds(self):
168
return ["rsync", "rhn", "yum",]
170
def get_release_files(self):
171
data = glob.glob(os.path.join(self.get_rootdir(), "*RELEASE"))
174
b = os.path.basename(x)
175
for valid_os in self.get_valid_os_versions():
176
if b.find(valid_os) != -1:
180
def get_tree_location(self, distro):
182
Once a distribution is identified, find the part of the distribution
183
that has the URL in it that we want to use for kickstarting the
184
distribution, and create a ksmeta variable $tree that contains this.
187
base = self.get_rootdir()
189
if self.network_root is None:
190
dest_link = os.path.join(self.settings.webdir, "links", distro.name)
191
# create the links directory only if we are mirroring because with
192
# SELinux Apache can't symlink to NFS (without some doing)
193
if not os.path.exists(dest_link):
195
os.symlink(base, dest_link)
197
# this shouldn't happen but I've seen it ... debug ...
198
self.logger.warning("symlink creation failed: %(base)s, %(dest)s") % { "base" : base, "dest" : dest_link }
199
# how we set the tree depends on whether an explicit network_root was specified
200
tree = "http://@@http_server@@/cblr/links/%s" % (distro.name)
201
self.set_install_tree(distro, tree)
203
# where we assign the answerfile source is relative to our current directory
204
# and the input start directory in the crawl. We find the path segments
205
# between and tack them on the network source path to find the explicit
206
# network path to the distro that Anaconda can digest.
207
tail = self.path_tail(self.path, base)
208
tree = self.network_root[:-1] + tail
209
self.set_install_tree(distro, tree)
213
def repo_finder(self, distros_added):
215
This routine looks through all distributions and tries to find
216
any applicable repositories in those distributions for post-install
220
for distro in distros_added:
221
self.logger.info("traversing distro %s" % distro.name)
222
# FIXME : Shouldn't decide this the value of self.network_root ?
223
if distro.kernel.find("ks_mirror") != -1:
224
basepath = os.path.dirname(distro.kernel)
225
top = self.get_rootdir()
226
self.logger.info("descent into %s" % top)
227
# FIXME : The location of repo definition is known from breed
228
os.path.walk(top, self.repo_scanner, distro)
230
self.logger.info("this distro isn't mirrored")
232
def repo_scanner(self,distro,dirname,fnames):
234
This is an os.path.walk routine that looks for potential yum repositories
235
to be added to the configuration for post-install usage.
240
if x == "base" or x == "repodata":
241
self.logger.info("processing repo at : %s" % dirname)
242
# only run the repo scanner on directories that contain a comps.xml
243
gloob1 = glob.glob("%s/%s/*comps*.xml" % (dirname,x))
245
if matches.has_key(dirname):
246
self.logger.info("looks like we've already scanned here: %s" % dirname)
248
self.logger.info("need to process repo/comps: %s" % dirname)
249
self.process_comps_file(dirname, distro)
252
self.logger.info("directory %s is missing xml comps file, skipping" % dirname)
255
def process_comps_file(self, comps_path, distro):
257
When importing Fedora/EL certain parts of the install tree can also be used
258
as yum repos containing packages that might not yet be available via updates
259
in yum. This code identifies those areas.
264
masterdir = "repodata"
265
if not os.path.exists(os.path.join(comps_path, "repodata")):
269
# figure out what our comps file is ...
270
self.logger.info("looking for %(p1)s/%(p2)s/*comps*.xml" % { "p1" : comps_path, "p2" : masterdir })
271
files = glob.glob("%s/%s/*comps*.xml" % (comps_path, masterdir))
273
self.logger.info("no comps found here: %s" % os.path.join(comps_path, masterdir))
274
return # no comps xml file found
276
# pull the filename from the longer part
277
comps_file = files[0].split("/")[-1]
280
# store the yum configs on the filesystem so we can use them later.
281
# and configure them in the answerfile post, etc
283
counter = len(distro.source_repos)
285
# find path segment for yum_url (changing filesystem path to http:// trailing fragment)
286
seg = comps_path.rfind("ks_mirror")
287
urlseg = comps_path[seg+10:]
289
# write a yum config file that shows how to use the repo.
291
dotrepo = "%s.repo" % distro.name
293
dotrepo = "%s-%s.repo" % (distro.name, counter)
295
fname = os.path.join(self.settings.webdir, "ks_mirror", "config", "%s-%s.repo" % (distro.name, counter))
297
repo_url = "http://@@http_server@@/cobbler/ks_mirror/config/%s-%s.repo" % (distro.name, counter)
298
repo_url2 = "http://@@http_server@@/cobbler/ks_mirror/%s" % (urlseg)
300
distro.source_repos.append([repo_url,repo_url2])
302
# NOTE: the following file is now a Cheetah template, so it can be remapped
303
# during sync, that's why we have the @@http_server@@ left as templating magic.
304
# repo_url2 is actually no longer used. (?)
306
config_file = open(fname, "w+")
307
config_file.write("[core-%s]\n" % counter)
308
config_file.write("name=core-%s\n" % counter)
309
config_file.write("baseurl=http://@@http_server@@/cobbler/ks_mirror/%s\n" % (urlseg))
310
config_file.write("enabled=1\n")
311
config_file.write("gpgcheck=0\n")
312
config_file.write("priority=$yum_distro_priority\n")
315
# don't run creatrepo twice -- this can happen easily for Xen and PXE, when
316
# they'll share same repo files.
318
if not processed_repos.has_key(comps_path):
319
utils.remove_yum_olddata(comps_path)
320
#cmd = "createrepo --basedir / --groupfile %s %s" % (os.path.join(comps_path, masterdir, comps_file), comps_path)
321
cmd = "createrepo %s --groupfile %s %s" % (self.settings.createrepo_flags,os.path.join(comps_path, masterdir, comps_file), comps_path)
322
utils.subprocess_call(self.logger, cmd, shell=True)
323
processed_repos[comps_path] = 1
324
# for older distros, if we have a "base" dir parallel with "repodata", we need to copy comps.xml up one...
325
p1 = os.path.join(comps_path, "repodata", "comps.xml")
326
p2 = os.path.join(comps_path, "base", "comps.xml")
327
if os.path.exists(p1) and os.path.exists(p2):
328
shutil.copyfile(p1,p2)
331
self.logger.error("error launching createrepo (not installed?), ignoring")
332
utils.log_exc(self.logger)
334
def distro_adder(self,distros_added,dirname,fnames):
336
This is an os.path.walk routine that finds distributions in the directory
337
to be scanned and then creates them.
346
fullname = os.path.join(dirname,x)
347
if os.path.islink(fullname) and os.path.isdir(fullname):
348
if fullname.startswith(self.path):
349
self.logger.warning("avoiding symlink loop")
351
self.logger.info("following symlink: %s" % fullname)
352
os.path.walk(fullname, self.distro_adder, distros_added)
354
if x == "mfsroot.gz":
355
initrd = os.path.join(dirname,x)
356
if x == "pxeboot" or x == "pxeboot.bs":
357
kernel = os.path.join(dirname,x)
359
# if we've collected a matching kernel and initrd pair, turn the in and add them to the list
360
if initrd is not None and kernel is not None:
361
adtls.append(self.add_entry(dirname,kernel,initrd))
366
distros_added.extend(adtl)
369
def add_entry(self,dirname,kernel,initrd):
371
When we find a directory with a valid kernel/initrd in it, create the distribution objects
372
as appropriate and save them. This includes creating xen and rescue distros/profiles
376
proposed_name = self.get_proposed_name(dirname,kernel)
377
proposed_arch = self.get_proposed_arch(dirname)
379
if self.arch and proposed_arch and self.arch != proposed_arch:
380
utils.die(self.logger,"Arch from pathname (%s) does not match with supplied one %s"%(proposed_arch,self.arch))
382
archs = self.learn_arch_from_tree()
385
archs.append( self.arch )
387
if self.arch and self.arch not in archs:
388
utils.die(self.logger, "Given arch (%s) not found on imported tree %s"%(self.arch,self.get_rootdir()))
390
if archs and proposed_arch not in archs:
391
self.logger.warning("arch from pathname (%s) not found on imported tree %s" % (proposed_arch,self.get_rootdir()))
394
archs = [ proposed_arch ]
397
self.logger.warning("- Warning : Multiple archs found : %s" % (archs))
401
for pxe_arch in archs:
402
name = proposed_name + "-" + pxe_arch
403
existing_distro = self.distros.find(name=name)
405
if existing_distro is not None:
406
self.logger.warning("skipping import, as distro name already exists: %s" % name)
410
self.logger.info("creating new distro: %s" % name)
411
distro = self.config.new_distro()
413
distro.set_name(name)
414
distro.set_kernel(kernel)
415
distro.set_initrd(initrd)
416
distro.set_arch(pxe_arch)
417
distro.set_breed(self.breed)
418
# If a version was supplied on command line, we set it now
420
distro.set_os_version(self.os_version)
422
self.distros.add(distro,save=True)
423
distros_added.append(distro)
425
existing_profile = self.profiles.find(name=name)
427
# see if the profile name is already used, if so, skip it and
428
# do not modify the existing profile
430
if existing_profile is None:
431
self.logger.info("creating new profile: %s" % name)
432
#FIXME: The created profile holds a default answerfile, and should be breed specific
433
profile = self.config.new_profile()
435
self.logger.info("skipping existing profile, name already exists: %s" % name)
438
# save our minimal profile which just points to the distribution and a good
439
# default answer file
441
profile.set_name(name)
442
profile.set_distro(name)
443
profile.set_kickstart(self.kickstart_file)
444
profile.set_virt_type("vmware")
446
# save our new profile to the collection
448
self.profiles.add(profile,save=True)
452
def get_proposed_name(self,dirname,kernel=None):
454
Given a directory name where we have a kernel/initrd pair, try to autoname
455
the distribution (and profile) object based on the contents of that path
458
if self.network_root is not None:
459
name = self.name + "-".join(self.path_tail(os.path.dirname(self.path),dirname).split("/"))
461
# remove the part that says /var/www/cobbler/ks_mirror/name
462
name = "-".join(dirname.split("/")[5:])
465
name = name.replace("-boot","")
467
for separator in [ '-' , '_' , '.' ] :
468
for arch in [ "i386" , "x86_64" , "ia64" , "ppc64", "ppc32", "ppc", "x86" , "s390x", "s390" , "386" , "amd" ]:
469
name = name.replace("%s%s" % ( separator , arch ),"")
473
def get_proposed_arch(self,dirname):
475
Given an directory name, can we infer an architecture from a path segment?
477
if dirname.find("x86_64") != -1 or dirname.find("amd") != -1:
479
if dirname.find("ia64") != -1:
481
if dirname.find("i386") != -1 or dirname.find("386") != -1 or dirname.find("x86") != -1:
483
if dirname.find("s390x") != -1:
485
if dirname.find("s390") != -1:
487
if dirname.find("ppc64") != -1 or dirname.find("chrp") != -1:
489
if dirname.find("ppc32") != -1:
491
if dirname.find("ppc") != -1:
495
def arch_walker(self,foo,dirname,fnames):
497
See docs on learn_arch_from_tree.
499
The TRY_LIST is used to speed up search, and should be dropped for default importer
500
Searched kernel names are kernel-header, linux-headers-, kernel-largesmp, kernel-hugemem
502
This method is useful to get the archs, but also to package type and a raw guess of the breed
505
# try to find a kernel header RPM and then look at it's arch.
507
if self.match_kernelarch_file(x):
508
for arch in self.get_valid_arches():
509
if x.find(arch) != -1:
511
for arch in [ "i686" , "amd64" ]:
512
if x.find(arch) != -1:
515
def kickstart_finder(self,distros_added):
517
For all of the profiles in the config w/o a answerfile, use the
518
given answerfile, or look at the kernel path, from that,
519
see if we can guess the distro, and if we can, assign a answerfile
520
if one is available for it.
522
for profile in self.profiles:
523
distro = self.distros.find(name=profile.get_conceptual_parent().name)
524
if distro is None or not (distro in distros_added):
527
kdir = os.path.dirname(distro.kernel)
528
if self.kickstart_file == None:
529
for rpm in self.get_release_files():
530
# FIXME : This redhat specific check should go into the importer.find_release_files method
531
if rpm.find("notes") != -1:
533
results = self.scan_pkg_filename(rpm)
534
# FIXME : If os is not found on tree but set with CLI, no answerfile is searched
536
self.logger.warning("No version found on imported tree")
538
(flavor, major, minor) = results
539
version , ks = self.set_variance(flavor, major, minor, distro.arch)
541
if self.os_version != version:
542
utils.die(self.logger,"CLI version differs from tree : %s vs. %s" % (self.os_version,version))
543
ds = self.get_datestamp()
544
distro.set_comment(version)
545
distro.set_os_version(version)
547
distro.set_tree_build_time(ds)
548
profile.set_kickstart(ks)
549
if flavor == "freebsd":
550
self.logger.info("This is FreeBSD - adding boot files to fetchable files")
551
# add fetchable files to distro
552
distro.set_fetchable_files('boot/mfsroot.gz=$initrd boot/*=$webdir/ks_mirror/$distro/boot/')
553
self.profiles.add(profile,save=True)
555
self.configure_tree_location(distro)
556
self.distros.add(distro,save=True) # re-save
559
def configure_tree_location(self, distro):
561
Once a distribution is identified, find the part of the distribution
562
that has the URL in it that we want to use for kickstarting the
563
distribution, and create a ksmeta variable $tree that contains this.
566
base = self.get_rootdir()
568
if self.network_root is None:
569
dest_link = os.path.join(self.settings.webdir, "links", distro.name)
570
# create the links directory only if we are mirroring because with
571
# SELinux Apache can't symlink to NFS (without some doing)
572
if not os.path.exists(dest_link):
574
os.symlink(base, dest_link)
576
# this shouldn't happen but I've seen it ... debug ...
577
self.logger.warning("symlink creation failed: %(base)s, %(dest)s") % { "base" : base, "dest" : dest_link }
578
# how we set the tree depends on whether an explicit network_root was specified
579
tree = "http://@@http_server@@/cblr/links/%s" % (distro.name)
580
self.set_install_tree( distro, tree)
582
# where we assign the kickstart source is relative to our current directory
583
# and the input start directory in the crawl. We find the path segments
584
# between and tack them on the network source path to find the explicit
585
# network path to the distro that Anaconda can digest.
586
tail = utils.path_tail(self.path, base)
587
tree = self.network_root[:-1] + tail
588
self.set_install_tree( distro, tree)
590
def get_rootdir(self):
593
def get_pkgdir(self):
596
return os.path.join(self.get_rootdir(),self.pkgdir)
598
def set_install_tree(self, distro, url):
599
distro.ks_meta["tree"] = url
601
def learn_arch_from_tree(self):
603
If a distribution is imported from DVD, there is a good chance the path doesn't
604
contain the arch and we should add it back in so that it's part of the
605
meaningful name ... so this code helps figure out the arch name. This is important
606
for producing predictable distro names (and profile names) from differing import sources
609
# FIXME : this is called only once, should not be a walk
610
if self.get_rootdir():
611
os.path.walk(self.get_rootdir(), self.arch_walker, result)
612
if result.pop("amd64",False):
614
if result.pop("i686",False):
618
def match_kernelarch_file(self, filename):
620
Is the given filename a kernel filename?
623
if not filename.endswith("rpm") and not filename.endswith("deb"):
625
for match in ["kernel-header", "kernel-source", "kernel-smp", "kernel-largesmp", "kernel-hugemem", "linux-headers-", "kernel-devel", "kernel-"]:
626
if filename.find(match) != -1:
630
def scan_pkg_filename(self, filename):
632
Determine what the distro is based on the release package filename.
634
release_file = os.path.basename(filename)
636
if release_file.lower().find("release") != -1:
638
match = re.search(r'(\d).(\d)-RELEASE', release_file)
640
major = match.group(1)
641
minor = match.group(2)
643
# FIXME: what should we do if the re fails above?
646
return (flavor, major, minor)
648
def get_datestamp(self):
650
Based on a FreeBSD tree find the creation timestamp
654
def set_variance(self, flavor, major, minor, arch):
656
find the profile answerfile and set the distro breed/os-version based on what
657
we can find out from the filenames and then return the answerfile
661
os_version = "%s%s.%s" % (flavor,major,minor)
662
answerfile = "/var/lib/cobbler/kickstarts/default.ks"
664
return os_version, answerfile
666
# ==========================================================================
668
def get_import_manager(config,logger):
669
return ImportFreeBSDManager(config,logger)