IF YOU WOULD LIKE TO GET AN ACCOUNT, please write an email to s dot adaszewski at gmail dot com. User accounts are meant only to report issues and/or generate pull requests. This is a purpose-specific Git hosting for ADARED projects. Thank you for your understanding!
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.

292 lines
9.7KB

  1. #
  2. # Copyright (C) Stanislaw Adaszewski, 2020
  3. # License: GNU General Public License v3.0
  4. # URL: https://github.com/sadaszewski/focker
  5. # URL: https://adared.ch/focker
  6. #
  7. import subprocess
  8. from .zfs import *
  9. import random
  10. import shutil
  11. import json
  12. from tabulate import tabulate
  13. import os
  14. import jailconf
  15. from .mount import getmntinfo
  16. import shlex
  17. # import pdb
  18. def jail_fs_create(image=None):
  19. sha256 = bytes([ random.randint(0, 255) for _ in range(32) ]).hex()
  20. lst = zfs_list(fields=['focker:sha256'], focker_type='image')
  21. lst = list(filter(lambda a: a[0] == sha256, lst))
  22. if lst:
  23. raise ValueError('Whew, a collision...')
  24. poolname = zfs_poolname()
  25. for pre in range(7, 32):
  26. name = poolname + '/focker/jails/' + sha256[:pre]
  27. if not zfs_exists(name):
  28. break
  29. if image:
  30. image, _ = zfs_find(image, focker_type='image', zfs_type='snapshot')
  31. zfs_parse_output(['zfs', 'clone', '-o', 'focker:sha256=' + sha256, image, name])
  32. else:
  33. print('Creating empty jail:', name)
  34. zfs_parse_output(['zfs', 'create', '-o', 'focker:sha256=' + sha256, name])
  35. return name
  36. def gen_env_command(command, env):
  37. if any(map(lambda a: ' ' in a, env.keys())):
  38. raise ValueError('Environment variable names cannot contain spaces')
  39. env = [ 'export ' + k + '=' + shlex.quote(v) \
  40. for (k, v) in env.items() ]
  41. command = ' && '.join(env + [ command ])
  42. return command
  43. def quote(s):
  44. s = s.replace('\\', '\\\\')
  45. s = s.replace('\'', '\\\'')
  46. s = '\'' + s + '\''
  47. return s
  48. def jail_create(path, command, env, mounts, hostname=None, overrides={}):
  49. name = os.path.split(path)[-1]
  50. if os.path.exists('/etc/jail.conf'):
  51. conf = jailconf.load('/etc/jail.conf')
  52. else:
  53. conf = jailconf.JailConf()
  54. conf[name] = blk = jailconf.JailBlock()
  55. blk['path'] = path
  56. if command:
  57. command = gen_env_command(command, env)
  58. command = quote(command)
  59. print('command:', command)
  60. blk['exec.start'] = command
  61. prestart = [ 'cp /etc/resolv.conf ' +
  62. shlex.quote(os.path.join(path, 'etc/resolv.conf')) ]
  63. poststop = []
  64. if mounts:
  65. for (from_, on) in mounts:
  66. if not from_.startswith('/'):
  67. from_, _ = zfs_find(from_, focker_type='volume')
  68. from_ = zfs_mountpoint(from_)
  69. prestart.append('mount -t nullfs ' + shlex.quote(from_) +
  70. ' ' + shlex.quote(os.path.join(path, on.strip('/'))))
  71. poststop += [ 'umount -f ' +
  72. os.path.join(path, on.strip('/')) \
  73. for (_, on) in reversed(mounts) ]
  74. if prestart:
  75. blk['exec.prestart'] = quote(' && '.join(prestart))
  76. if poststop:
  77. blk['exec.poststop'] = quote(' && '.join(poststop))
  78. blk['persist'] = True
  79. blk['interface'] = 'lo1'
  80. blk['ip4.addr'] = '127.0.1.0'
  81. blk['mount.devfs'] = True
  82. blk['exec.clean'] = True
  83. blk['host.hostname'] = hostname or name
  84. for (k, v) in overrides.items():
  85. blk[k] = quote(v)
  86. conf.write('/etc/jail.conf')
  87. return name
  88. def get_jid(path):
  89. data = json.loads(subprocess.check_output(['jls', '--libxo=json']))
  90. lst = data['jail-information']['jail']
  91. lst = list(filter(lambda a: a['path'] == path, lst))
  92. if len(lst) == 0:
  93. raise ValueError('JID not found for path: ' + path)
  94. if len(lst) > 1:
  95. raise ValueError('Ambiguous JID for path: ' + path)
  96. return str(lst[0]['jid'])
  97. def do_mounts(path, mounts):
  98. print('mounts:', mounts)
  99. for (source, target) in mounts:
  100. if source.startswith('/'):
  101. name = source
  102. else:
  103. name, _ = zfs_find(source, focker_type='volume')
  104. name = zfs_mountpoint(name)
  105. while target.startswith('/'):
  106. target = target[1:]
  107. subprocess.check_output(['mount', '-t', 'nullfs',
  108. shlex.quote(name), shlex.quote(os.path.join(path, target))])
  109. def undo_mounts(path, mounts):
  110. for (_, target) in reversed(mounts):
  111. while target.startswith('/'):
  112. target = target[1:]
  113. subprocess.check_output(['umount', '-f',
  114. shlex.quote(os.path.join(path, target))])
  115. def jail_run(path, command, mounts=[]):
  116. command = ['jail', '-c', 'host.hostname=' + os.path.split(path)[1], 'persist=1', 'mount.devfs=1', 'interface=lo1', 'ip4.addr=127.0.1.0', 'path=' + path, 'command', '/bin/sh', '-c', command]
  117. print('Running:', ' '.join(command))
  118. try:
  119. do_mounts(path, mounts)
  120. shutil.copyfile('/etc/resolv.conf', os.path.join(path, 'etc/resolv.conf'))
  121. res = subprocess.run(command)
  122. finally:
  123. try:
  124. subprocess.run(['jail', '-r', get_jid(path)])
  125. except ValueError:
  126. pass
  127. subprocess.run(['umount', '-f', os.path.join(path, 'dev')])
  128. undo_mounts(path, mounts)
  129. if res.returncode != 0:
  130. # subprocess.run(['umount', os.path.join(path, 'dev')])
  131. raise RuntimeError('Command failed')
  132. def jail_stop(path):
  133. try:
  134. jid = get_jid(path)
  135. jailname = os.path.split(path)[-1]
  136. subprocess.run(['jail', '-r', jailname])
  137. except ValueError:
  138. print('JID could not be determined')
  139. # import time
  140. # time.sleep(1)
  141. mi = getmntinfo()
  142. for m in mi:
  143. mntonname = m['f_mntonname'].decode('utf-8')
  144. if mntonname.startswith(path + os.path.sep):
  145. print('Unmounting:', mntonname)
  146. subprocess.run(['umount', '-f', mntonname])
  147. def jail_remove(path):
  148. print('Removing jail:', path)
  149. jail_stop(path)
  150. subprocess.run(['zfs', 'destroy', '-r', '-f', zfs_name(path)])
  151. if os.path.exists('/etc/jail.conf'):
  152. conf = jailconf.load('/etc/jail.conf')
  153. name = os.path.split(path)[-1]
  154. if name in conf:
  155. del conf[name]
  156. conf.write('/etc/jail.conf')
  157. def command_jail_create(args):
  158. name = jail_fs_create(args.image)
  159. if args.tags:
  160. zfs_tag(name, args.tags)
  161. path = zfs_mountpoint(name)
  162. jail_create(path, args.command,
  163. { a.split(':')[0]: ':'.join(a.split(':')[1:]) \
  164. for a in args.env },
  165. [ [a.split(':')[0], ':'.join(a.split(':')[1:])] \
  166. for a in args.mounts ],
  167. args.hostname )
  168. # print(sha256)
  169. print(path)
  170. def command_jail_start(args):
  171. name, _ = zfs_find(args.reference, focker_type='jail')
  172. path = zfs_mountpoint(name)
  173. jailname = os.path.split(path)[-1]
  174. subprocess.run(['jail', '-c', jailname])
  175. def command_jail_stop(args):
  176. name, _ = zfs_find(args.reference, focker_type='jail')
  177. path = zfs_mountpoint(name)
  178. jail_stop(path)
  179. def command_jail_remove(args):
  180. name, _ = zfs_find(args.reference, focker_type='jail')
  181. path = zfs_mountpoint(name)
  182. jail_remove(path)
  183. def command_jail_exec(args):
  184. name, _ = zfs_find(args.reference, focker_type='jail')
  185. path = zfs_mountpoint(name)
  186. jid = get_jid(path)
  187. subprocess.run(['jexec', str(jid)] + args.command)
  188. def jail_oneshot(image, command, env, mounts):
  189. # pdb.set_trace()
  190. name = jail_fs_create(image)
  191. path = zfs_mountpoint(name)
  192. jailname = jail_create(path,
  193. ' '.join(map(shlex.quote, command or ['/bin/sh'])),
  194. env, mounts)
  195. subprocess.run(['jail', '-c', jailname])
  196. jail_remove(path)
  197. def command_jail_oneshot(args):
  198. env = { a.split(':')[0]: ':'.join(a.split(':')[1:]) \
  199. for a in args.env }
  200. mounts = [ [ a.split(':')[0], a.split(':')[1] ] \
  201. for a in args.mounts]
  202. jail_oneshot(args.image, args.command, env, mounts)
  203. # Deprecated
  204. def command_jail_oneshot_old():
  205. base, _ = zfs_snapshot_by_tag_or_sha256(args.image)
  206. # root = '/'.join(base.split('/')[:-1])
  207. for _ in range(10**6):
  208. sha256 = bytes([ random.randint(0, 255) for _ in range(32) ]).hex()
  209. name = sha256[:7]
  210. name = base.split('/')[0] + '/focker/jails/' + name
  211. if not zfs_exists(name):
  212. break
  213. zfs_run(['zfs', 'clone', '-o', 'focker:sha256=' + sha256, base, name])
  214. try:
  215. mounts = list(map(lambda a: a.split(':'), args.mounts))
  216. jail_run(zfs_mountpoint(name), args.command, mounts)
  217. # subprocess.check_output(['jail', '-c', 'interface=lo1', 'ip4.addr=127.0.1.0', 'path=' + zfs_mountpoint(name), 'command', command])
  218. finally:
  219. # subprocess.run(['umount', zfs_mountpoint(name) + '/dev'])
  220. zfs_run(['zfs', 'destroy', '-f', name])
  221. # raise
  222. def command_jail_list(args):
  223. lst = zfs_list(fields=['focker:sha256,focker:tags,mountpoint'], focker_type='jail')
  224. jails = subprocess.check_output(['jls', '--libxo=json'])
  225. jails = json.loads(jails)['jail-information']['jail']
  226. jails = { j['path']: j for j in jails }
  227. lst = list(map(lambda a: [ a[1],
  228. a[0] if args.full_sha256 else a[0][:7],
  229. a[2],
  230. jails[a[2]]['jid'] if a[2] in jails else '-' ], lst))
  231. print(tabulate(lst, headers=['Tags', 'SHA256', 'mountpoint', 'JID']))
  232. def command_jail_tag(args):
  233. name, _ = zfs_find(args.reference, focker_type='jail')
  234. zfs_untag(args.tags, focker_type='jail')
  235. zfs_tag(name, args.tags)
  236. def command_jail_untag(args):
  237. zfs_untag(args.tags, focker_type='jail')
  238. def command_jail_prune(args):
  239. jails = subprocess.check_output(['jls', '--libxo=json'])
  240. jails = json.loads(jails)['jail-information']['jail']
  241. used = set()
  242. for j in jails:
  243. used.add(j['path'])
  244. lst = zfs_list(fields=['focker:sha256,focker:tags,mountpoint,name'], focker_type='jail')
  245. for j in lst:
  246. if j[1] == '-' and (j[2] not in used or args.force):
  247. jail_remove(j[2])