generate 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. #!/bin/env python
  2. """
  3. Attempts to read 'master-config.yaml' and extract remote
  4. host information to dynamically create an inventory file
  5. in order to run Ansible playbooks against that host.
  6. """
  7. import os
  8. import re
  9. import shlex
  10. import shutil
  11. import subprocess
  12. import sys
  13. import yaml
  14. try:
  15. HOME = os.environ['HOME']
  16. except KeyError:
  17. print 'A required environment variable "$HOME" has not been set'
  18. exit(1)
  19. DEFAULT_USER_CONFIG_PATH = '/etc/inventory-generator-config.yaml'
  20. DEFAULT_MASTER_CONFIG_PATH = HOME + '/master-config.yaml'
  21. DEFAULT_ADMIN_KUBECONFIG_PATH = HOME + '/.kube/config'
  22. INVENTORY_FULL_PATH = HOME + '/generated_hosts'
  23. USE_STDOUT = True
  24. if len(sys.argv) > 1:
  25. INVENTORY_FULL_PATH = sys.argv[1]
  26. USE_STDOUT = False
  27. class OpenShiftClientError(Exception):
  28. """Base exception class for OpenShift CLI wrapper"""
  29. pass
  30. class InvalidHost(Exception):
  31. """Base exception class for host creation problems."""
  32. pass
  33. class InvalidHostGroup(Exception):
  34. """Base exception class for host-group creation problems."""
  35. pass
  36. class OpenShiftClient:
  37. oc = None
  38. kubeconfig = None
  39. def __init__(self, kubeconfig=DEFAULT_ADMIN_KUBECONFIG_PATH):
  40. """Find and store path to oc binary"""
  41. # https://github.com/openshift/openshift-ansible/issues/3410
  42. # oc can be in /usr/local/bin in some cases, but that may not
  43. # be in $PATH due to ansible/sudo
  44. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ['/usr/local/bin', os.path.expanduser('~/bin')]
  45. oc_binary_name = 'oc'
  46. oc_binary = None
  47. # Use shutil.which if it is available, otherwise fallback to a naive path search
  48. try:
  49. which_result = shutil.which(oc_binary_name, path=os.pathsep.join(paths))
  50. if which_result is not None:
  51. oc_binary = which_result
  52. except AttributeError:
  53. for path in paths:
  54. if os.path.exists(os.path.join(path, oc_binary_name)):
  55. oc_binary = os.path.join(path, oc_binary_name)
  56. break
  57. if oc_binary is None:
  58. raise OpenShiftClientError('Unable to locate `oc` binary. Not present in PATH.')
  59. self.oc = oc_binary
  60. self.kubeconfig = kubeconfig
  61. def call(self, cmd_str):
  62. """Execute a remote call using `oc`"""
  63. cmd = [
  64. self.oc,
  65. '--kubeconfig',
  66. self.kubeconfig
  67. ] + shlex.split(cmd_str)
  68. try:
  69. out = subprocess.check_output(list(cmd), stderr=subprocess.STDOUT)
  70. except subprocess.CalledProcessError as err:
  71. raise OpenShiftClientError('[rc {}] {}\n{}'.format(err.returncode, ' '.join(err.cmd), err.output))
  72. return out
  73. def whoami(self):
  74. """Retrieve information about the current user in the given kubeconfig"""
  75. return self.call('whoami')
  76. def get_nodes(self):
  77. """Retrieve remote node information as a yaml object"""
  78. return self.call('get nodes -o yaml')
  79. class HostGroup:
  80. groupname = ""
  81. hosts = list()
  82. def __init__(self, hosts):
  83. if not hosts:
  84. return
  85. first = hosts[0].get_group_name()
  86. for h in hosts:
  87. if h.get_group_name() != first:
  88. raise InvalidHostGroup("Attempt to create HostGroup with hosts of varying groups.")
  89. self.hosts = hosts
  90. self.groupname = first
  91. def add_host(self, host):
  92. """Add a new host to this group."""
  93. self.hosts.append(host)
  94. def get_group_name(self):
  95. """Return the groupname associated with each aggregated host."""
  96. return self.groupname
  97. def get_hosts(self):
  98. """Return aggregated hosts"""
  99. return self.hosts
  100. def string(self):
  101. """Call the print method for each aggregated host; separated by newlines."""
  102. infos = ""
  103. for host in self.hosts:
  104. infos += host.string() + "\n"
  105. return infos
  106. class Host:
  107. group = "masters"
  108. alias = ""
  109. hostname = ""
  110. public_hostname = ""
  111. ip_addr = ""
  112. public_ip_addr = ""
  113. def __init__(self, groupname):
  114. if not groupname:
  115. raise InvalidHost("Attempt to create Host with no group name provided.")
  116. self.group = groupname
  117. def get_group_name(self):
  118. return self.group
  119. def get_openshift_hostname(self):
  120. return self.hostname
  121. def host_alias(self, hostalias):
  122. """Set an alias for this host."""
  123. self.alias = hostalias
  124. def address(self, ip):
  125. """Set the ip address for this host."""
  126. self.ip_addr = ip
  127. def public_address(self, ip):
  128. """Set the external ip address for this host."""
  129. self.public_ip_addr = ip
  130. def host_name(self, hname):
  131. self.hostname = parse_hostname(hname)
  132. def public_host_name(self, phname):
  133. self.public_hostname = parse_hostname(phname)
  134. def string(self):
  135. """Print an inventory-file compatible string with host information"""
  136. info = ""
  137. if self.alias:
  138. info += self.alias + " "
  139. elif self.hostname:
  140. info += self.hostname + " "
  141. elif self.ip_addr:
  142. info += self.ip_addr + " "
  143. if self.ip_addr:
  144. info += "openshift_ip=" + self.ip_addr + " "
  145. if self.public_ip_addr:
  146. info += "openshift_public_ip=" + self.public_ip_addr + " "
  147. if self.hostname:
  148. info += "openshift_kubelet_name_override=" + self.hostname + " "
  149. if self.public_hostname:
  150. info += "openshift_public_hostname=" + self.public_hostname
  151. return info
  152. def parse_hostname(host):
  153. """Remove protocol and port from given hostname.
  154. Return parsed string"""
  155. no_proto = re.split('^http(s)?\:\/\/', host)
  156. if no_proto:
  157. host = no_proto[-1]
  158. no_port = re.split('\:[0-9]+(/)?$', host)
  159. if no_port:
  160. host = no_port[0]
  161. return host
  162. def main():
  163. """Parse master-config file and populate inventory file."""
  164. # set default values
  165. USER_CONFIG = os.environ.get('CONFIG')
  166. if not USER_CONFIG:
  167. USER_CONFIG = DEFAULT_USER_CONFIG_PATH
  168. # read user configuration
  169. try:
  170. config_file_obj = open(USER_CONFIG, 'r')
  171. raw_config_file = config_file_obj.read()
  172. user_config = yaml.load(raw_config_file)
  173. if not user_config:
  174. user_config = dict()
  175. except IOError as err:
  176. print "Unable to find or read user configuration file '{}': {}".format(USER_CONFIG, err)
  177. exit(1)
  178. master_config_path = user_config.get('master_config_path', DEFAULT_MASTER_CONFIG_PATH)
  179. if not master_config_path:
  180. master_config_path = DEFAULT_MASTER_CONFIG_PATH
  181. admin_kubeconfig_path = user_config.get('admin_kubeconfig_path', DEFAULT_ADMIN_KUBECONFIG_PATH)
  182. if not admin_kubeconfig_path:
  183. admin_kubeconfig_path = DEFAULT_ADMIN_KUBECONFIG_PATH
  184. try:
  185. file_obj = open(master_config_path, 'r')
  186. except IOError as err:
  187. print "Unable to find or read host master configuration file '{}': {}".format(master_config_path, err)
  188. exit(1)
  189. raw_text = file_obj.read()
  190. y = yaml.load(raw_text)
  191. if y.get("kind", "") != "MasterConfig":
  192. print "Bind-mounted host master configuration file is not of 'kind' MasterConfig. Aborting..."
  193. exit(1)
  194. # finish reading config file and begin gathering
  195. # cluster information for inventory file
  196. file_obj.close()
  197. # set inventory values based on user configuration
  198. ansible_ssh_user = user_config.get('ansible_ssh_user', 'root')
  199. ansible_become_user = user_config.get('ansible_become_user')
  200. openshift_uninstall_images = user_config.get('openshift_uninstall_images', False)
  201. openshift_install_examples = user_config.get('openshift_install_examples', True)
  202. openshift_deployment_type = user_config.get('openshift_deployment_type', 'origin')
  203. openshift_release = user_config.get('openshift_release')
  204. openshift_image_tag = user_config.get('openshift_image_tag')
  205. openshift_logging_image_version = user_config.get('openshift_logging_image_version')
  206. openshift_disable_check = user_config.get('openshift_disable_check')
  207. # extract host config info from parsed yaml file
  208. asset_config = y.get("assetConfig")
  209. master_config = y.get("kubernetesMasterConfig")
  210. etcd_config = y.get("etcdClientInfo")
  211. # if master_config is missing, error out; we expect to be running on a master to be able to
  212. # gather enough information to generate the rest of the inventory file.
  213. if not master_config:
  214. msg = "'kubernetesMasterConfig' missing from '{}'; unable to gather all necessary host information..."
  215. print msg.format(master_config_path)
  216. exit(1)
  217. master_public_url = y.get("masterPublicURL")
  218. if not master_public_url:
  219. msg = "'kubernetesMasterConfig.masterPublicURL' missing from '{}'; Unable to connect to master host..."
  220. print msg.format(master_config_path)
  221. exit(1)
  222. oc = OpenShiftClient(admin_kubeconfig_path)
  223. # ensure kubeconfig is logged in with provided user, or fail with a friendly message otherwise
  224. try:
  225. oc.whoami()
  226. except OpenShiftClientError as err:
  227. msg = ("Unable to obtain user information using the provided kubeconfig file. "
  228. "Current context does not appear to be able to authenticate to the server. "
  229. "Error returned from server:\n\n{}")
  230. print msg.format(str(err))
  231. exit(1)
  232. # connect to remote host using the provided config and extract all possible node information
  233. nodes_config = yaml.load(oc.get_nodes())
  234. # contains host types (e.g. masters, nodes, etcd)
  235. host_groups = dict()
  236. openshift_logging_install_logging = False
  237. is_etcd_deployed = master_config.get("storage-backend", "") in ["etcd3", "etcd2", "etcd"]
  238. if asset_config and asset_config.get('loggingPublicURL'):
  239. openshift_logging_install_logging = True
  240. openshift_logging_install_logging = user_config.get("openshift_logging_install_logging", openshift_logging_install_logging)
  241. m = Host("masters")
  242. m.address(master_config["masterIP"])
  243. m.public_host_name(master_public_url)
  244. host_groups["masters"] = HostGroup([m])
  245. if nodes_config:
  246. node_hosts = list()
  247. for node in nodes_config.get("items", []):
  248. if node["kind"] != "Node":
  249. continue
  250. n = Host("nodes")
  251. address = ""
  252. internal_hostname = ""
  253. for item in node["status"].get("addresses", []):
  254. if not address and item['type'] in ['InternalIP', 'LegacyHostIP']:
  255. address = item['address']
  256. if item['type'] == 'Hostname':
  257. internal_hostname = item['address']
  258. n.address(address)
  259. n.host_name(internal_hostname)
  260. node_hosts.append(n)
  261. host_groups["nodes"] = HostGroup(node_hosts)
  262. if etcd_config:
  263. etcd_hosts = list()
  264. for url in etcd_config.get("urls", []):
  265. e = Host("etcd")
  266. e.host_name(url)
  267. etcd_hosts.append(e)
  268. host_groups["etcd"] = HostGroup(etcd_hosts)
  269. # open new inventory file for writing
  270. if USE_STDOUT:
  271. inv_file_obj = sys.stdout
  272. else:
  273. try:
  274. inv_file_obj = open(INVENTORY_FULL_PATH, 'w+')
  275. except IOError as err:
  276. print "Unable to create or open generated inventory file: {}".format(err)
  277. exit(1)
  278. inv_file_obj.write("[OSEv3:children]\n")
  279. for group in host_groups:
  280. inv_file_obj.write("{}\n".format(group))
  281. inv_file_obj.write("\n")
  282. inv_file_obj.write("[OSEv3:vars]\n")
  283. if ansible_ssh_user:
  284. inv_file_obj.write("ansible_ssh_user={}\n".format(ansible_ssh_user))
  285. if ansible_become_user:
  286. inv_file_obj.write("ansible_become_user={}\n".format(ansible_become_user))
  287. inv_file_obj.write("ansible_become=yes\n")
  288. if openshift_uninstall_images:
  289. inv_file_obj.write("openshift_uninstall_images={}\n".format(str(openshift_uninstall_images)))
  290. if openshift_deployment_type:
  291. inv_file_obj.write("openshift_deployment_type={}\n".format(openshift_deployment_type))
  292. if openshift_install_examples:
  293. inv_file_obj.write("openshift_install_examples={}\n".format(str(openshift_install_examples)))
  294. if openshift_release:
  295. inv_file_obj.write("openshift_release={}\n".format(str(openshift_release)))
  296. if openshift_image_tag:
  297. inv_file_obj.write("openshift_image_tag={}\n".format(str(openshift_image_tag)))
  298. if openshift_logging_image_version:
  299. inv_file_obj.write("openshift_logging_image_version={}\n".format(str(openshift_logging_image_version)))
  300. if openshift_disable_check:
  301. inv_file_obj.write("openshift_disable_check={}\n".format(str(openshift_disable_check)))
  302. inv_file_obj.write("\n")
  303. inv_file_obj.write("openshift_logging_install_logging={}\n".format(str(openshift_logging_install_logging)))
  304. inv_file_obj.write("\n")
  305. for group in host_groups:
  306. inv_file_obj.write("[{}]\n".format(host_groups[group].get_group_name()))
  307. inv_file_obj.write(host_groups[group].string())
  308. inv_file_obj.write("\n")
  309. inv_file_obj.close()
  310. if __name__ == '__main__':
  311. main()