Advertisement
Guest User

Untitled

a guest
Feb 13th, 2017
134
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.49 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. # lxc_bootstrap
  4. # Copyright (C) 2017 Thomas Ward <teward@ubuntu.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18.  
  19. # LXC Bootstrapper, around the lxc-create 'Download' template for userspace
  20. # containers; creates then modifies the container based on specifications.
  21. #
  22. # Designed for Ubuntu / Debian systems.
  23.  
  24. # import os
  25. import sys
  26. import argparse
  27. import crypt
  28. import subprocess as sp
  29. import random
  30. import platform
  31. from string import ascii_letters, digits
  32.  
  33. SHADOW_SALT_CHARSET = ascii_letters + digits
  34.  
  35. ARCHITECTURE_MAP = {
  36. 'amd64': 'amd64', # Explicit definition
  37. 'i386': 'i386', # Explicit definition
  38. 'armhf': 'armhf', # Explicit definition
  39. 'arm64': 'arm64', # Explicit definition
  40. 'x86_64': 'amd64',
  41. 'x86': 'i386',
  42. 'armv7l': 'armhf',
  43. 'armv8l': 'arm64'
  44. }
  45.  
  46.  
  47. # noinspection PyClassHasNoInit
  48. class Bootstrap:
  49. # noinspection PyClassHasNoInit
  50. class Container:
  51. bootstrap_existing = False
  52. name = None
  53. architecture = None
  54. distribution = None
  55. release = None
  56.  
  57. # noinspection PyClassHasNoInit
  58. class Customization:
  59. # noinspection PyClassHasNoInit
  60. class Packages:
  61. to_add = ['openssh-server', 'software-properties-common', 'haveged', 'python', 'python-dev',
  62. 'python3', 'python3-dev', 'perl-modules', 'ubuntu-server', 'iptables', 'libnetfilter-conntrack3']
  63. to_remove = ['lxd', 'lxd-client', 'lxd-tools', 'lxc']
  64. autoremove_after = False
  65.  
  66. # noinspection PyClassHasNoInit
  67. class Users:
  68. userdata = [('teward', '47AlphaTango@2012', None, True)]
  69.  
  70.  
  71. def _parse_arguments():
  72. argparser = argparse.ArgumentParser(description="LXC Container Bootstrapper Assistant", add_help=True)
  73. argparser.add_argument('-e', '--existing', '--bootstrap-existing', dest="container_bootstrap_existing",
  74. default=False, required=False, action='store_true',
  75. help="Don't create a container, run bootstrapping on "
  76. "an already-existing container.")
  77. argparser.add_argument('-n', '--name', type=str, dest="container_name", required=True,
  78. help="The name to assign to the LXC container.")
  79. argparser.add_argument('-a', '--arch', type=str, dest="container_arch", default=None, required=False,
  80. help="The architecture for the container")
  81. argparser.add_argument('-d', '--dist', '--distro', type=str, dest="container_dist", default=None, required=False,
  82. help="The distribution for the container")
  83. argparser.add_argument('-r', '--release', '--codename', type=str, dest="container_release", default=None,
  84. required=False, help="The specific release of the container")
  85. argparser.add_argument('--add-packages', type=str, dest="packages_add", required=False,
  86. default='openssh-server,software-properties-common,haveged,python,python-dev,'
  87. 'python3,python3-dev,perl-modules,ubuntu-server,iptables',
  88. help="Comma-separated list of packages to add to the container.")
  89. argparser.add_argument('--exclude-packages', type=str, dest="packages_exclude", required=False,
  90. default='lxd,lxd-client,lxd-tools,lxc',
  91. help="Comma-separated list of packages to exclude from the container.")
  92. argparser.add_argument('--users', '--userdata', '--userfile', type=str, dest="user_datafile", required=False,
  93. default=None,
  94. help="Path to a file containing user data, one user per line in USERNAME:PASSWORD:SALT:ADMIN"
  95. " format, where SALT is an optional 8-character alpha numeric string, and ADMIN is "
  96. "'True' or 'False'")
  97.  
  98. return argparser.parse_args()
  99.  
  100.  
  101. # noinspection PyShadowingNames
  102. def _get_bootstrap(args):
  103. if type(args) is not argparse.Namespace:
  104. raise TypeError("Invalid arguments provided.")
  105. tmpbootstrap = Bootstrap()
  106.  
  107. tmpbootstrap.Container.name = str(args.container_name).strip(''')
  108.  
  109. tmpbootstrap.Container.bootstrap_existing = args.container_bootstrap_existing
  110.  
  111. if not tmpbootstrap.Container.bootstrap_existing:
  112. if 'Windows' in platform.platform():
  113. raise OSError("LXC doesn't work on Windows, so we can't use this script. Sorry!")
  114. elif 'Linux' not in platform.platform():
  115. raise OSError("This script only works for Linux OSes, sorry!")
  116.  
  117. if args.container_arch:
  118. tmpbootstrap.Container.architecture = args.container_arch
  119. else:
  120. tmpbootstrap.Container.architecture = ARCHITECTURE_MAP[platform.machine()]
  121.  
  122. if args.container_dist:
  123. tmpbootstrap.Container.distribution = args.container_dist
  124. else:
  125. lsb_dist = sp.Popen('/usr/bin/lsb_release -s -i'.split(), stdout=sp.PIPE, stderr=sp.PIPE).communicate()
  126. if lsb_dist[1]:
  127. raise SystemError("Error getting release distributor ID.")
  128. else:
  129. tmpbootstrap.Container.distribution = str(lsb_dist[0]).lower().strip('rn')
  130.  
  131. if args.container_release:
  132. tmpbootstrap.Container.release = args.container_release
  133. else:
  134. try:
  135. lsb_codename = sp.Popen('/usr/bin/lsb_release -s -c'.split(),
  136. stdout=sp.PIPE, stderr=sp.PIPE).communicate()
  137. if lsb_codename[1]:
  138. raise SystemError("Error getting release codename from lsb_release")
  139. tmpbootstrap.Container.release = str(lsb_codename[0]).lower().strip('rn')
  140. except Exception as e:
  141. raise e
  142.  
  143. if args.packages_add:
  144. addpackages = args.packages_add.split(',')
  145. for pkg in addpackages:
  146. if pkg.lower() not in tmpbootstrap.Customization.Packages.to_add:
  147. tmpbootstrap.Customization.Packages.to_add.append(pkg.lower())
  148.  
  149. if args.packages_exclude:
  150. delpackages = args.packages_exclude.split(',')
  151. for pkg in delpackages:
  152. if pkg.lower() in tmpbootstrap.Customization.Packages.to_add:
  153. tmpbootstrap.Customization.Packages.to_add.remove(pkg.lower())
  154. else:
  155. if pkg.lower() not in tmpbootstrap.Customization.Packages.to_remove:
  156. tmpbootstrap.Customization.Packages.to_remove.append(pkg.lower())
  157.  
  158. if args.user_datafile:
  159. datafile = open(args.user_datafile, 'r')
  160. for data in datafile:
  161. data = data.split(':')
  162. if len(data) != 4:
  163. raise IOError("Invalid data provided from datafile.")
  164. else:
  165. datatuple = (data[0], data[1], data[2], bool(data[3]))
  166. # noinspection PyTypeChecker
  167. tmpbootstrap.Customization.Users.userdata.append(datatuple)
  168.  
  169. return tmpbootstrap
  170.  
  171.  
  172. def _create_container(data):
  173. lxc_create_cmd = str("/usr/bin/lxc-create -t download -n %s -- -d %s -r %s -a %s" % (
  174. data.name, data.distribution, data.release, data.architecture)).split()
  175.  
  176. try:
  177. # noinspection PyUnusedLocal
  178. ecode = sp.check_call(lxc_create_cmd, stdout=sys.stdout, stderr=sys.stderr)
  179. except sp.CalledProcessError:
  180. print "Something went wrong when creating the container."
  181. exit()
  182.  
  183.  
  184. def _start_container(container_name):
  185. execstr = str('lxc-start -n %s' % container_name).split()
  186. try:
  187. # noinspection PyUnusedLocal
  188. ecode = sp.check_call(execstr, stdout=sys.stdout, stderr=sys.stderr)
  189. except sp.CalledProcessError:
  190. print "Could not start the container, cannot continue with bootstrap."
  191. exit()
  192.  
  193.  
  194. def _shadow_pw_str(password, salt=None):
  195. if not salt or len(salt) != 8:
  196. # Create a random salt
  197. for _ in range(8):
  198. try:
  199. salt += random.SystemRandom().choice(SHADOW_SALT_CHARSET)
  200. except (TypeError, UnboundLocalError):
  201. salt = random.SystemRandom().choice(SHADOW_SALT_CHARSET)
  202.  
  203. hashed = crypt.crypt(password, ('$6$%s$' % salt))
  204. return hashed
  205.  
  206.  
  207. # noinspection PyUnreachableCode,PyUnusedLocal
  208. def _container_bootstrap_users(container_name, data):
  209. # Default comes with 'ubuntu' user and group; let's nuke them
  210. del_default_usr = "/usr/bin/lxc-attach -n %s -- deluser --remove-all-files ubuntu" % container_name
  211. del_default_grp = "/usr/bin/lxc-attach -n %s -- delgroup --only-if-empty ubuntu" % container_name
  212.  
  213. try:
  214. deldefusr = sp.check_call(del_default_usr.split(), stdout=sys.stdout, stderr=sys.stderr)
  215. except sp.CalledProcessError:
  216. print "Could not delete default user and/or group, please refer to error logs."
  217.  
  218. try:
  219. deldefgrp = sp.check_call(del_default_grp.split(), stdout=sys.stdout, stderr=sys.stderr)
  220. except sp.CalledProcessError as e:
  221. if e.returncode == 5:
  222. print "Could not delete default group, it's not empty!"
  223. elif e.returncode == 3:
  224. pass # return code of 3 means that the group doesn't exist, so we can move on.
  225.  
  226. # Since we just nuked the '1000' group and user (default Ubuntu), let's start there now.
  227. uid = 1000
  228. gid = 1000
  229.  
  230. for (username, password, salt, admin) in data.userdata:
  231. hashpw = _shadow_pw_str(password, salt)
  232.  
  233. groupcreatecmd = "lxc-attach -n %s -- groupadd -g %s %s" % (container_name, gid, username)
  234.  
  235. usrcreatecmd = ("lxc-attach -n %s -- useradd --create-home -u %s -g %s -p '%s' --shell=/bin/bash %s"
  236. % (container_name, uid, gid, hashpw, username))
  237.  
  238. try:
  239. groupcmd = sp.check_call(groupcreatecmd.split(), stdout=sys.stdout, stderr=sys.stderr)
  240. usercmd = sp.check_call(usrcreatecmd.split(), stdout=sys.stdout, stderr=sys.stderr)
  241. if admin:
  242. usermodcmd = "lxc-attach -n %s -- usermod -a -G sudo %s" % (container_name, username)
  243. usermod = sp.check_call(usermodcmd.split(), stdout=sys.stdout, stderr=sys.stderr)
  244.  
  245. except sp.CalledProcessError:
  246. print "Something went wrong when bootstrapping user '%s'..." % username
  247.  
  248. uid += 1
  249. gid += 1
  250.  
  251.  
  252. # noinspection PyUnusedLocal
  253. def _container_bootstrap_packages(container_name, data, autoremove=False):
  254. # Construct string
  255. basecommand = '/usr/bin/lxc-attach -n %s -- apt-get ' % container_name
  256. installstr = basecommand + "install -y %s" % " ".join(data.to_add)
  257. removestr = basecommand + "remove -y --purge %s" % " ".join(data.to_remove)
  258. autoremove = basecommand + "autoremove -y --purge"
  259.  
  260. try:
  261. ecode = sp.call(installstr.split(), stdout=sys.stdout, stderr=sys.stderr)
  262. except sp.CalledProcessError:
  263. print "Something went wrong installing additional packages."
  264. exit()
  265.  
  266. try:
  267. ecode = sp.call(removestr.split(), stdout=sys.stdout, stderr=sys.stderr)
  268. except sp.CalledProcessError:
  269. print "Something went wrong removing specified packages."
  270. exit()
  271.  
  272. if autoremove:
  273. try:
  274. ecode = sp.call(autoremove.split(), stdout=sys.stdout, stderr=sys.stderr)
  275. except sp.CalledProcessError:
  276. print "Something went wrong cleaning up after removal with 'autoremove'."
  277.  
  278.  
  279. def run():
  280. # Get args first, we'll pass this to the Bootstrapper.
  281. # noinspection PyUnreachableCode
  282. args = _parse_arguments()
  283.  
  284. # Take the arguments, throw it into _get_bootstrap, and get a Bootstrap object.
  285. bootstrap = _get_bootstrap(args)
  286.  
  287. if not bootstrap.Container.bootstrap_existing:
  288. # Create the container
  289. _create_container(bootstrap.Container)
  290. else:
  291. # Do nothing, continue on with bootstrap process.
  292. pass
  293.  
  294. _start_container(bootstrap.Container.name)
  295.  
  296. _container_bootstrap_packages(bootstrap.Container.name, bootstrap.Customization.Packages)
  297.  
  298. _container_bootstrap_users(bootstrap.Container.name, bootstrap.Customization.Users)
  299.  
  300.  
  301. if __name__ == "__main__":
  302. run()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement