cluster 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #!/usr/bin/env python2
  2. # vim: expandtab:tabstop=4:shiftwidth=4
  3. import argparse
  4. import ConfigParser
  5. import os
  6. import sys
  7. import subprocess
  8. import traceback
  9. class Cluster(object):
  10. """
  11. Provide Command, Control and Configuration (c3) Interface for OpenShift Clusters
  12. """
  13. def __init__(self):
  14. # setup ansible ssh environment
  15. if 'ANSIBLE_SSH_ARGS' not in os.environ:
  16. os.environ['ANSIBLE_SSH_ARGS'] = (
  17. '-o ForwardAgent=yes '
  18. '-o StrictHostKeyChecking=no '
  19. '-o UserKnownHostsFile=/dev/null '
  20. '-o ControlMaster=auto '
  21. '-o ControlPersist=600s '
  22. )
  23. # Because of `UserKnownHostsFile=/dev/null`
  24. # our `.ssh/known_hosts` file most probably misses the ssh host public keys
  25. # of our servers.
  26. # In that case, ansible serializes the execution of ansible modules
  27. # because we might be interactively prompted to accept the ssh host public keys.
  28. # Because of `StrictHostKeyChecking=no` we know that we won't be prompted
  29. # So, we don't want our modules execution to be serialized.
  30. os.environ['ANSIBLE_HOST_KEY_CHECKING'] = 'False'
  31. # TODO: A more secure way to proceed would consist in dynamically
  32. # retrieving the ssh host public keys from the IaaS interface
  33. def get_deployment_type(self, args):
  34. """
  35. Get the deployment_type based on the environment variables and the
  36. command line arguments
  37. :param args: command line arguments provided by the user
  38. :return: string representing the deployment type
  39. """
  40. deployment_type = 'origin'
  41. if args.deployment_type:
  42. deployment_type = args.deployment_type
  43. elif 'OS_DEPLOYMENT_TYPE' in os.environ:
  44. deployment_type = os.environ['OS_DEPLOYMENT_TYPE']
  45. return deployment_type
  46. def create(self, args):
  47. """
  48. Create an OpenShift cluster for given provider
  49. :param args: command line arguments provided by user
  50. """
  51. env = {'cluster_id': args.cluster_id,
  52. 'deployment_type': self.get_deployment_type(args)}
  53. playbook = "playbooks/{0}/openshift-cluster/launch.yml".format(args.provider)
  54. inventory = self.setup_provider(args.provider)
  55. env['num_masters'] = args.masters
  56. env['num_nodes'] = args.nodes
  57. env['num_infra'] = args.infra
  58. env['num_etcd'] = args.etcd
  59. self.action(args, inventory, env, playbook)
  60. def addNodes(self, args):
  61. """
  62. Add nodes to an existing cluster for given provider
  63. :param args: command line arguments provided by user
  64. """
  65. env = {'cluster_id': args.cluster_id,
  66. 'deployment_type': self.get_deployment_type(args)}
  67. playbook = "playbooks/{0}/openshift-cluster/addNodes.yml".format(args.provider)
  68. inventory = self.setup_provider(args.provider)
  69. env['num_nodes'] = args.nodes
  70. env['num_infra'] = args.infra
  71. self.action(args, inventory, env, playbook)
  72. def terminate(self, args):
  73. """
  74. Destroy OpenShift cluster
  75. :param args: command line arguments provided by user
  76. """
  77. env = {'cluster_id': args.cluster_id,
  78. 'deployment_type': self.get_deployment_type(args)}
  79. playbook = "playbooks/{0}/openshift-cluster/terminate.yml".format(args.provider)
  80. inventory = self.setup_provider(args.provider)
  81. self.action(args, inventory, env, playbook)
  82. def list(self, args):
  83. """
  84. List VMs in cluster
  85. :param args: command line arguments provided by user
  86. """
  87. env = {'cluster_id': args.cluster_id,
  88. 'deployment_type': self.get_deployment_type(args)}
  89. playbook = "playbooks/{0}/openshift-cluster/list.yml".format(args.provider)
  90. inventory = self.setup_provider(args.provider)
  91. self.action(args, inventory, env, playbook)
  92. def config(self, args):
  93. """
  94. Configure or reconfigure OpenShift across clustered VMs
  95. :param args: command line arguments provided by user
  96. """
  97. env = {'cluster_id': args.cluster_id,
  98. 'deployment_type': self.get_deployment_type(args)}
  99. playbook = "playbooks/{0}/openshift-cluster/config.yml".format(args.provider)
  100. inventory = self.setup_provider(args.provider)
  101. self.action(args, inventory, env, playbook)
  102. def update(self, args):
  103. """
  104. Update to latest OpenShift across clustered VMs
  105. :param args: command line arguments provided by user
  106. """
  107. env = {'cluster_id': args.cluster_id,
  108. 'deployment_type': self.get_deployment_type(args)}
  109. playbook = "playbooks/{0}/openshift-cluster/update.yml".format(args.provider)
  110. inventory = self.setup_provider(args.provider)
  111. self.action(args, inventory, env, playbook)
  112. def service(self, args):
  113. """
  114. Make the same service call across all nodes in the cluster
  115. :param args: command line arguments provided by user
  116. """
  117. env = {'cluster_id': args.cluster_id,
  118. 'deployment_type': self.get_deployment_type(args),
  119. 'new_cluster_state': args.state}
  120. playbook = "playbooks/{0}/openshift-cluster/service.yml".format(args.provider)
  121. inventory = self.setup_provider(args.provider)
  122. self.action(args, inventory, env, playbook)
  123. def setup_provider(self, provider):
  124. """
  125. Setup ansible playbook environment
  126. :param provider: command line arguments provided by user
  127. :return: path to inventory for given provider
  128. """
  129. config = ConfigParser.ConfigParser()
  130. if 'gce' == provider:
  131. gce_ini_default_path = os.path.join(
  132. 'inventory/gce/hosts/gce.ini')
  133. gce_ini_path = os.environ.get('GCE_INI_PATH', gce_ini_default_path)
  134. if os.path.exists(gce_ini_path):
  135. config.readfp(open(gce_ini_path))
  136. for key in config.options('gce'):
  137. os.environ[key] = config.get('gce', key)
  138. inventory = '-i inventory/gce/hosts'
  139. elif 'aws' == provider:
  140. config.readfp(open('inventory/aws/hosts/ec2.ini'))
  141. for key in config.options('ec2'):
  142. os.environ[key] = config.get('ec2', key)
  143. inventory = '-i inventory/aws/hosts'
  144. key_vars = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']
  145. key_missing = [key for key in key_vars if key not in os.environ]
  146. boto_conf_files = ['~/.aws/credentials', '~/.boto']
  147. conf_exists = lambda conf: os.path.isfile(os.path.expanduser(conf))
  148. boto_configs = [conf for conf in boto_conf_files if conf_exists(conf)]
  149. if len(key_missing) > 0 and len(boto_configs) == 0:
  150. raise ValueError("PROVIDER aws requires {0} environment variable(s). See README_AWS.md".format(key_missing))
  151. elif 'libvirt' == provider:
  152. inventory = '-i inventory/libvirt/hosts'
  153. elif 'openstack' == provider:
  154. inventory = '-i inventory/openstack/hosts'
  155. else:
  156. # this code should never be reached
  157. raise ValueError("invalid PROVIDER {0}".format(provider))
  158. return inventory
  159. def action(self, args, inventory, env, playbook):
  160. """
  161. Build ansible-playbook command line and execute
  162. :param args: command line arguments provided by user
  163. :param inventory: derived provider library
  164. :param env: environment variables for kubernetes
  165. :param playbook: ansible playbook to execute
  166. """
  167. verbose = ''
  168. if args.verbose > 0:
  169. verbose = '-{0}'.format('v' * args.verbose)
  170. if args.option:
  171. for opt in args.option:
  172. k, v = opt.split('=', 1)
  173. env['cli_' + k] = v
  174. ansible_env = '-e \'{0}\''.format(
  175. ' '.join(['%s=%s' % (key, value) for (key, value) in env.items()])
  176. )
  177. command = 'ansible-playbook {0} {1} {2} {3}'.format(
  178. verbose, inventory, ansible_env, playbook
  179. )
  180. if args.profile:
  181. command = 'ANSIBLE_CALLBACK_PLUGINS=ansible-profile/callback_plugins ' + command
  182. if args.verbose > 1:
  183. command = 'time {0}'.format(command)
  184. if args.verbose > 0:
  185. sys.stderr.write('RUN [{0}]\n'.format(command))
  186. sys.stderr.flush()
  187. try:
  188. subprocess.check_call(command, shell=True)
  189. except subprocess.CalledProcessError as exc:
  190. raise ActionFailed("ACTION [{0}] failed: {1}"
  191. .format(args.action, exc))
  192. class ActionFailed(Exception):
  193. """
  194. Raised when action failed.
  195. """
  196. pass
  197. if __name__ == '__main__':
  198. """
  199. User command to invoke ansible playbooks in a "known" environment
  200. Reads ~/.openshift-ansible for default configuration items
  201. [DEFAULT]
  202. validate_cluster_ids = False
  203. cluster_ids = marketing,sales
  204. providers = gce,aws,libvirt,openstack
  205. """
  206. environment = ConfigParser.SafeConfigParser({
  207. 'cluster_ids': 'marketing,sales',
  208. 'validate_cluster_ids': 'False',
  209. 'providers': 'gce,aws,libvirt,openstack',
  210. })
  211. path = os.path.expanduser("~/.openshift-ansible")
  212. if os.path.isfile(path):
  213. environment.read(path)
  214. cluster = Cluster()
  215. parser = argparse.ArgumentParser(
  216. description='Python wrapper to ensure proper environment for OpenShift ansible playbooks',
  217. )
  218. parser.add_argument('-v', '--verbose', action='count',
  219. help='Multiple -v options increase the verbosity')
  220. parser.add_argument('--version', action='version', version='%(prog)s 0.3')
  221. meta_parser = argparse.ArgumentParser(add_help=False)
  222. providers = environment.get('DEFAULT', 'providers').split(',')
  223. meta_parser.add_argument('provider', choices=providers, help='provider')
  224. if environment.get('DEFAULT', 'validate_cluster_ids').lower() in ("yes", "true", "1"):
  225. meta_parser.add_argument('cluster_id', choices=environment.get('DEFAULT', 'cluster_ids').split(','),
  226. help='prefix for cluster VM names')
  227. else:
  228. meta_parser.add_argument('cluster_id', help='prefix for cluster VM names')
  229. meta_parser.add_argument('-t', '--deployment-type',
  230. choices=['origin', 'online', 'enterprise'],
  231. help='Deployment type. (default: origin)')
  232. meta_parser.add_argument('-T', '--product-type',
  233. choices=['openshift', 'atomic-enterprise'],
  234. help='Product type. (default: openshift)')
  235. meta_parser.add_argument('-o', '--option', action='append',
  236. help='options')
  237. meta_parser.add_argument('-p', '--profile', action='store_true',
  238. help='Enable playbook profiling')
  239. action_parser = parser.add_subparsers(dest='action', title='actions',
  240. description='Choose from valid actions')
  241. create_parser = action_parser.add_parser('create', help='Create a cluster',
  242. parents=[meta_parser])
  243. create_parser.add_argument('-m', '--masters', default=1, type=int,
  244. help='number of masters to create in cluster')
  245. create_parser.add_argument('-n', '--nodes', default=2, type=int,
  246. help='number of nodes to create in cluster')
  247. create_parser.add_argument('-i', '--infra', default=1, type=int,
  248. help='number of infra nodes to create in cluster')
  249. create_parser.add_argument('-e', '--etcd', default=0, type=int,
  250. help='number of external etcd hosts to create in cluster')
  251. create_parser.set_defaults(func=cluster.create)
  252. create_parser = action_parser.add_parser('addNodes', help='Add nodes to a cluster',
  253. parents=[meta_parser])
  254. create_parser.add_argument('-n', '--nodes', default=1, type=int,
  255. help='number of nodes to add to the cluster')
  256. create_parser.add_argument('-i', '--infra', default=1, type=int,
  257. help='number of infra nodes to add to the cluster')
  258. create_parser.set_defaults(func=cluster.addNodes)
  259. config_parser = action_parser.add_parser('config',
  260. help='Configure or reconfigure a cluster',
  261. parents=[meta_parser])
  262. config_parser.set_defaults(func=cluster.config)
  263. terminate_parser = action_parser.add_parser('terminate',
  264. help='Destroy a cluster',
  265. parents=[meta_parser])
  266. terminate_parser.add_argument('-f', '--force', action='store_true',
  267. help='Destroy cluster without confirmation')
  268. terminate_parser.set_defaults(func=cluster.terminate)
  269. update_parser = action_parser.add_parser('update',
  270. help='Update OpenShift across cluster',
  271. parents=[meta_parser])
  272. update_parser.add_argument('-f', '--force', action='store_true',
  273. help='Update cluster without confirmation')
  274. update_parser.set_defaults(func=cluster.update)
  275. list_parser = action_parser.add_parser('list', help='List VMs in cluster',
  276. parents=[meta_parser])
  277. list_parser.set_defaults(func=cluster.list)
  278. service_parser = action_parser.add_parser('service', help='service for openshift across cluster',
  279. parents=[meta_parser])
  280. # choices are the only ones valid for the ansible service module: http://docs.ansible.com/service_module.html
  281. service_parser.add_argument('state', choices=['started', 'stopped', 'restarted', 'reloaded'],
  282. help='make service call across cluster')
  283. service_parser.set_defaults(func=cluster.service)
  284. args = parser.parse_args()
  285. if 'terminate' == args.action and not args.force:
  286. answer = raw_input("This will destroy the ENTIRE {0} environment. Are you sure? [y/N] ".format(args.cluster_id))
  287. if answer not in ['y', 'Y']:
  288. sys.stderr.write('\nACTION [terminate] aborted by user!\n')
  289. exit(1)
  290. if 'update' == args.action and not args.force:
  291. answer = raw_input(
  292. "This is destructive and could corrupt {0} environment. Continue? [y/N] ".format(args.cluster_id))
  293. if answer not in ['y', 'Y']:
  294. sys.stderr.write('\nACTION [update] aborted by user!\n')
  295. exit(1)
  296. try:
  297. args.func(args)
  298. except Exception as exc:
  299. if args.verbose:
  300. traceback.print_exc(file=sys.stderr)
  301. else:
  302. print >>sys.stderr, exc
  303. exit(1)