cluster 13 KB

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