No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

genconfdrv.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function
  4. import argparse
  5. import fs.tempfs
  6. import fs.path
  7. import ipaddress
  8. import json
  9. import os
  10. import subprocess
  11. import sys
  12. import uuid
  13. __VERSION__ = '0.1'
  14. class ConfigDrive:
  15. def __init__(self, genisoimage='/usr/bin/genisoimage', verbose=False):
  16. self._tmpfs = None
  17. self._genisoimage = genisoimage
  18. self._user_data = {} # {"system_info": {"default_user": None}}
  19. self._interfaces = []
  20. self._pubkeys = []
  21. self._verbose = verbose
  22. self._clean_metadata = False
  23. self._added_resolv_module_call = False
  24. if not os.path.exists(genisoimage):
  25. print("Error: %s does not exist, no genisoimage found!" % genisoimage, file=sys.stderr)
  26. sys.exit(1)
  27. self.open()
  28. def set_hostname(self, hostname):
  29. self._hostname = hostname
  30. def conf_network(self, interface, address=None, gateway=None, *extra_routes):
  31. if not address and gateway:
  32. raise ValueError("You cannot define a gateway, but supply no address")
  33. if not self._interfaces:
  34. self._interfaces.extend([
  35. "auto lo",
  36. "iface lo inet loopback",
  37. ])
  38. if address:
  39. if address == "dhcp":
  40. address = None
  41. method = "dhcp"
  42. elif "/" in address:
  43. address = ipaddress.ip_interface(address)
  44. method = "static"
  45. else:
  46. raise ValueError("IP Interface is not a subnet")
  47. else:
  48. method = "manual"
  49. self._interfaces.extend([
  50. "",
  51. "auto %s" % interface,
  52. "iface %s inet%s %s" % (interface, "" if not address or address.version == 4 else "6", method),
  53. ])
  54. if address:
  55. self._interfaces.append(" address %s" % address)
  56. if gateway:
  57. self._interfaces.append(" gateway %s" % str(ipaddress.ip_address(gateway)))
  58. for routedef in extra_routes:
  59. if "-" not in routedef:
  60. raise ValueError("Route {} is missing a gateway separated by a -".format(routedef))
  61. route, gw = routedef.split('-')
  62. if "/" not in route:
  63. raise ValueError("Route {} is not a subnet".format(route))
  64. route = ipaddress.ip_interface(route)
  65. gw = ipaddress.ip_address(gw)
  66. self._interfaces.append(" up ip route add {} via {}".format(route, gw))
  67. def conf_resolve(self, resolvers):
  68. if not self._hostname:
  69. raise ValueError("Please set a hostname before calling this function")
  70. for n, resolver in enumerate(resolvers, 1):
  71. try:
  72. ipaddress.ip_address(resolver)
  73. except ValueError as e:
  74. print("Nameserver argument %s: %s" % (n, e), file=sys.stderr)
  75. sys.exit(1)
  76. self._user_data["manage_resolv_conf"] = True
  77. self._user_data["resolv_conf"] = {
  78. "nameservers": resolvers,
  79. }
  80. if "." in self._hostname:
  81. self._user_data["domain"] = ".".join(self._hostname.split(".")[1:])
  82. # debian, by default, does not call the cc_resolv_conf module
  83. if not self._added_resolv_module_call:
  84. self.add_command("cloud-init single --name cc_resolv_conf", True)
  85. self._added_resolv_module_call = True
  86. def set_clean_metadata(self, do_clean_metadata):
  87. self._clean_metadata = do_clean_metadata
  88. def add_user(self, name, keys=None, gecos=None, sudo=False, password=None):
  89. if "users" not in self._user_data:
  90. self._user_data["users"] = []
  91. user = {
  92. "name": name,
  93. "shell": "/bin/bash",
  94. "home": "/home/%s" % name
  95. }
  96. if keys:
  97. if type(keys) == str:
  98. keys = [keys]
  99. user["ssh_authorized_keys"] = keys
  100. if gecos:
  101. user["gecos"] = gecos
  102. if sudo:
  103. user["sudo"] = "ALL=(ALL) NOPASSWD:ALL"
  104. if password:
  105. raise NotImplementedError("crypt, salt, $6$ something")
  106. self._user_data["users"].append(user)
  107. def add_fp(self, path, fp):
  108. self.add_text(path, fp.read())
  109. def add_text(self, path, content):
  110. dir_path = fs.path.dirname(path)
  111. if dir_path and not self._tmpfs.exists(dir_path):
  112. self._tmpfs.makedirs(dir_path)
  113. self._tmpfs.settext(path, content)
  114. if self._verbose:
  115. print(" >>", path)
  116. print(content)
  117. print()
  118. def open(self):
  119. if not self._tmpfs:
  120. self._tmpfs = fs.tempfs.TempFS("genconfdrv", auto_clean=True)
  121. def _write_metadata(self):
  122. if not self._hostname:
  123. raise ValueError("No hostname set")
  124. meta_data = {
  125. # "availability_zone": "cat",
  126. "files": [],
  127. "hostname": self._hostname,
  128. "name": self._hostname.split(".")[0],
  129. # "meta": {
  130. # "role": "webservers",
  131. # "essential": False,
  132. # }
  133. "uuid": str(uuid.uuid4()),
  134. }
  135. if self._interfaces:
  136. # add the source-directory to interfaces
  137. self._interfaces.extend([
  138. "",
  139. "source-directory /etc/network/interfaces.d/",
  140. "",
  141. ])
  142. meta_data["files"].append({"content_path": "/content/0000", "path": "/etc/network/interfaces"})
  143. self.add_text("/openstack/content/0000", "\n".join(self._interfaces))
  144. meta_data["files"].append({"content_path": "/content/0001",
  145. "path": "/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg"})
  146. self.add_text("/openstack/content/0001", "network: {config: disabled}")
  147. # do not look for datasource on every boot
  148. if self._clean_metadata:
  149. meta_data["files"].append({"content_path": "/content/0002",
  150. "path": "/etc/cloud/cloud.cfg.d/99-manual-cache-clean.cfg"})
  151. self.add_text("/openstack/content/0002", "manual_cache_clean: True")
  152. if self._pubkeys:
  153. meta_data["public_keys"] = {}
  154. for n, key in enumerate(self._pubkeys):
  155. meta_data["public_keys"]["key-%02d" % n] = key
  156. self.add_text("/openstack/latest/meta_data.json", json.dumps(meta_data, indent=4))
  157. def enable_upgrades(self):
  158. self._user_data["package_update"] = True
  159. self._user_data["package_upgrade"] = True
  160. def add_command(self, command, boot=True):
  161. key = "bootcmd" if boot else "runcmd"
  162. if key not in self._user_data:
  163. self._user_data[key] = []
  164. self._user_data[key].append(command)
  165. def add_pubkey(self, pubkey):
  166. self._pubkeys.append(pubkey)
  167. def set_password(self, user, password):
  168. if "chpasswd" not in self._user_data:
  169. self._user_data["chpasswd"] = {}
  170. self._user_data["chpasswd"]["list"] = ""
  171. # self._user_data["chpasswd"]["list"] = []
  172. # self._user_data["chpasswd"]["list"].append("%s:%s" % (user, password))
  173. self._user_data["chpasswd"]["list"] += "%s:%s\n" % (user, password)
  174. def _write_userdata(self):
  175. self.add_text("/openstack/latest/user_data", "#cloud-config\n" + json.dumps(self._user_data, indent=4))
  176. def write_drive(self, path, fmt):
  177. self._write_metadata()
  178. self._write_userdata()
  179. if fmt == "iso":
  180. self._write_iso(path)
  181. elif fmt == "tgz":
  182. self._write_tgz(path)
  183. else:
  184. raise ValueError("Unknown format")
  185. def _write_iso(self, path):
  186. p = subprocess.Popen([self._genisoimage,
  187. "-J", "-r", "-q",
  188. "-V", "config-2",
  189. "-publisher", "seba-genconfdrv"
  190. "-l", "-ldots", "-allow-lowercase", "-allow-multidot",
  191. "-input-charset", "utf-8",
  192. "-o", path,
  193. self._tmpfs.getsyspath(""),
  194. ])
  195. return p.wait()
  196. def _write_tgz(self, path):
  197. p = subprocess.Popen(["tar", "cfz", path, "-C", self._tmpfs.getsyspath(""), "."])
  198. return p.wait()
  199. def close(self):
  200. if self._tmpfs:
  201. self._tmpfs.close()
  202. # defaults for testing
  203. # cfgdrv.set_hostname("foo.someserver.de")
  204. # cfgdrv.conf_network("ens3", "172.23.0.4/24", "172.23.0.1")
  205. # cfgdrv.conf_resolve(["1.1.1.1", "8.8.8.8"])
  206. # cfgdrv.enable_upgrades()
  207. # cfgdrv.add_command("rm -rf /home/debian/; userdel debian; groupdel debian", True)
  208. # cfgdrv.add_command("cloud-init single --name cc_resolv_conf", True)
  209. # cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*.cfg", True)
  210. # cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl; "
  211. # "rm /etc/apt/sources.list.d/*", True)
  212. # #cfgdrv.add_command("(whoami; date) > /root/bleep", False)
  213. # cfgdrv.add_pubkey("ssh-rsa bleep foo")
  214. # cfgdrv.set_password("root", "kitteh")
  215. def main():
  216. parser = argparse.ArgumentParser()
  217. parser.add_argument("-H", "--hostname", required=True, help="Hostname")
  218. parser.add_argument("-o", "--output", required=True, help="Path to write iso to")
  219. parser.add_argument("-n", "--nameservers", "--ns", default=["1.1.1.1", "8.8.8.8"], nargs="+", help="Nameservers")
  220. parser.add_argument("-i", "--networks", "--net", default=[], nargs="+",
  221. help="Specify all networks, in format of interface[:address[:gateway[:route-gateway[:...]]]]. "
  222. "Both : and ; can be used as delimiter (but only one per net config). "
  223. "Address MUST be a network in CIDR notation or dhcp for DHCP mode. "
  224. "Additional routes can be added in the form of cidr-gateway, e.g. "
  225. "10.0.0.0/8-10.0.0.1")
  226. parser.add_argument("-u", "--disable-upgrades", action="store_true", default=False)
  227. parser.add_argument("-v", "--verbose", action="store_true", default=False)
  228. parser.add_argument("--no-debian-cleanup", "--ndc", action="store_true", default=False)
  229. parser.add_argument("--no-debian-sources-cleanup", "--ndsc", action="store_true", default=False)
  230. parser.add_argument("--no-remove-cloud-init", action="store_true", default=False,
  231. help="Do not purge cloud-init from system after execution")
  232. parser.add_argument("--set-root-password", "--srp", default=None)
  233. parser.add_argument("-a", "--add-user", default=[], nargs="+",
  234. help="Add users, format is username:key?:sudo?:gecos?:password?, "
  235. "sudo is a bool, key is either an ssh key or a path to an ssh key")
  236. parser.add_argument("-f", "--format", default=None, choices=('tgz', 'iso'),
  237. help="Specify output format, default is to infer from output file extension")
  238. args = parser.parse_args()
  239. if not args.format:
  240. if args.output.endswith(".tar.gz") or args.output.endswith(".tgz"):
  241. args.format = "tgz"
  242. elif args.output.endswith(".iso"):
  243. args.format = "iso"
  244. else:
  245. parser.error("Could not infer output format from output file extension")
  246. cfgdrv = None
  247. try:
  248. cfgdrv = ConfigDrive(verbose=args.verbose)
  249. cfgdrv.set_hostname(args.hostname)
  250. for net in args.networks:
  251. if ";" in net:
  252. net = net.split(";")
  253. else:
  254. net = net.split(":")
  255. cfgdrv.conf_network(*net)
  256. if args.nameservers:
  257. cfgdrv.conf_resolve(args.nameservers)
  258. if not args.disable_upgrades:
  259. cfgdrv.enable_upgrades()
  260. if not args.no_debian_cleanup:
  261. cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*", True)
  262. cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl", True)
  263. cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' "
  264. "/etc/resolv.conf /etc/cloud/templates/resolv.conf.tmpl", True)
  265. if not args.no_debian_sources_cleanup:
  266. cfgdrv.add_command("rm /etc/apt/sources.list.d/*", True)
  267. if args.set_root_password:
  268. cfgdrv.set_password("root", args.set_root_password)
  269. if args.add_user:
  270. for user in args.add_user:
  271. user = user.split(":")
  272. # user key sudo gecos password
  273. if len(user) < 2:
  274. parser.error("Missing key parameter for user")
  275. keys = ""
  276. if len(user) >= 2:
  277. if user[1].startswith("ssh-"):
  278. keys = [user[1]]
  279. else:
  280. with open(os.path.expanduser(user[1])) as keyfile:
  281. keys = keyfile.read().split("\n")
  282. sudo = True
  283. if len(user) >= 3:
  284. sudo = user[2] not in (False, 0, "0", "no", "false", "False")
  285. gecos = None
  286. if len(user) >= 4:
  287. gecos = user[3]
  288. password = None
  289. if len(user) >= 5:
  290. password = user[4]
  291. cfgdrv.add_user(user[0], keys, sudo=sudo, gecos=gecos, password=password)
  292. if not args.no_remove_cloud_init:
  293. cfgdrv.add_command("apt remove -y cloud-init")
  294. if args.output:
  295. cfgdrv.write_drive(args.output, args.format)
  296. finally:
  297. if cfgdrv:
  298. cfgdrv.close()
  299. if __name__ == '__main__':
  300. main()