build-image.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import yaml
  2. import argparse
  3. import uuid
  4. import os
  5. import sys
  6. import subprocess
  7. import shutil
  8. def mkdir_p(path):
  9. try:
  10. os.makedirs(path)
  11. except OSError:
  12. pass
  13. class Image:
  14. def __init__(self, **kwargs):
  15. self.basepath = None
  16. self.parent = None
  17. self.config = {}
  18. for key, value in kwargs.iteritems():
  19. if key == "basepath":
  20. self.basepath = value
  21. if key == "config":
  22. self.config = value
  23. if key == "parent":
  24. self.parent = value
  25. if not self.basepath:
  26. raise ValueError("No basepath specified")
  27. if not self.config.has_key('repositories'):
  28. self.config['repositories'] = []
  29. self.dnf_dir = os.path.join(self.basepath, "dnf")
  30. self.installroot = os.path.join(self.basepath, "build")
  31. if self.parent:
  32. self.config['repositories'].extend(self.parent.config['repositories'])
  33. self.config['releasever'] = self.parent.config['releasever']
  34. def build(self):
  35. self.copy_parent()
  36. self.configure_dnf()
  37. self.install_packages()
  38. self.create_files()
  39. self.append_files()
  40. self.enable_units()
  41. self.run_scripts()
  42. def copy_parent(self):
  43. if not self.parent:
  44. return
  45. if os.path.exists(self.installroot):
  46. return
  47. shutil.copytree(self.parent.installroot, self.installroot, symlinks=True)
  48. def configure_dnf(self):
  49. mkdir_p(self.dnf_dir)
  50. configfile = "[main]\ngpgcheck=1\ninstallonly_limit=3\nclean_requirements_on_remove=True\nreposdir=do-not-use\n"
  51. with open(os.path.join(self.dnf_dir, 'dnf.conf'), 'w') as stream:
  52. stream.write(configfile)
  53. for repo in self.config['repositories']:
  54. stream.write(repo)
  55. def install_packages(self):
  56. try:
  57. if len(self.config['packages']) == 0:
  58. return
  59. except KeyError:
  60. return
  61. command = [ 'dnf', '-y', '-c', os.path.join(self.dnf_dir, 'dnf.conf'), "--releasever=%s" % self.config["releasever"], "--installroot=%s" % self.installroot, 'install' ]
  62. command.extend(self.config["packages"])
  63. retval = subprocess.call(command)
  64. if retval > 0:
  65. raise RuntimeError("DNF failed")
  66. def enable_units(self):
  67. try:
  68. if len(self.config['enableunits']) == 0:
  69. return
  70. except KeyError:
  71. return
  72. command = [ 'systemd-nspawn', '-D', self.installroot, 'systemctl', 'enable' ]
  73. command.extend(self.config['enableunits'])
  74. retval = subprocess.call(command)
  75. if retval > 0:
  76. raise RuntimeError("Failed to enable units")
  77. def create_files(self):
  78. try:
  79. if len(self.config['files']) == 0:
  80. return
  81. except KeyError:
  82. return
  83. for name, contents in self.config['files'].iteritems():
  84. mkdir_p(os.path.dirname(self.installroot + name))
  85. with open(self.installroot + name, 'w') as stream:
  86. stream.write(contents)
  87. def append_files(self):
  88. try:
  89. if len(self.config['appendfiles']) == 0:
  90. return
  91. except KeyError:
  92. return
  93. for name, contents in self.config['appendfiles'].iteritems():
  94. mkdir_p(os.path.dirname(self.installroot + name))
  95. with open(self.installroot + name, 'a') as stream:
  96. stream.write(contents)
  97. def run_scripts(self):
  98. try:
  99. if len(self.config['postscripts']) == 0:
  100. return
  101. except KeyError:
  102. return
  103. command = [ 'systemd-nspawn', '-D', self.installroot, 'sh', '/postscript' ]
  104. for script in self.config['postscripts']:
  105. with open(self.installroot + '/postscript', 'w') as stream:
  106. stream.write(script)
  107. retval = subprocess.call(command)
  108. os.remove(self.installroot + '/postscript')
  109. if retval > 0:
  110. raise RuntimeError("Failed to run postscript")
  111. def main():
  112. parser = argparse.ArgumentParser(description='Build an nspawn image.')
  113. parser.add_argument('--basedir',
  114. dest = 'basedir',
  115. default = os.getcwd(),
  116. help = 'Base directory to build in (defaults to current directory)'
  117. )
  118. parser.add_argument('--configfile',
  119. dest = 'configfile',
  120. default = os.path.join(os.getcwd(), "images.yml"),
  121. help = 'Build definition file (defaults to images.yml)'
  122. )
  123. args = parser.parse_args()
  124. #session_uuid = uuid.uuid4()
  125. session_uuid = "build"
  126. session_dir = os.path.join(os.path.abspath(args.basedir), str(session_uuid))
  127. configfile = os.path.abspath(args.configfile)
  128. configdata = {}
  129. with open(configfile, 'r') as stream:
  130. try:
  131. configdata = yaml.load(stream)
  132. except yaml.YAMLError as exc:
  133. print(exc)
  134. sys.exit(1)
  135. images_ordered = []
  136. while True:
  137. unresolved = False
  138. for image in configdata:
  139. if configdata[image] in images_ordered:
  140. continue
  141. if configdata[image].has_key('baseimage'):
  142. if not configdata[configdata[image]['baseimage']] in images_ordered:
  143. unresolved = True
  144. continue
  145. configdata[image]['name'] = image
  146. images_ordered.append(configdata[image])
  147. if not unresolved:
  148. break
  149. mkdir_p(session_dir)
  150. images = {}
  151. for image in images_ordered:
  152. image_basepath = os.path.join(session_dir, image['name'])
  153. mkdir_p(image_basepath)
  154. if image.has_key('baseimage'):
  155. parent = images[image['baseimage']]
  156. else:
  157. parent = None
  158. i = Image(basepath = image_basepath, config = image, parent = parent)
  159. images[image['name']] = i
  160. print("Building %s" % image['name'])
  161. i.build()
  162. if __name__ == "__main__":
  163. main()