cluster 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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. if 'ANSIBLE_SSH_PIPELINING' not in os.environ:
  34. os.environ['ANSIBLE_SSH_PIPELINING'] = 'True'
  35. def get_deployment_type(self, args):
  36. """
  37. Get the deployment_type based on the environment variables and the
  38. command line arguments
  39. :param args: command line arguments provided by the user
  40. :return: string representing the deployment type
  41. """
  42. deployment_type = 'origin'
  43. if args.deployment_type:
  44. deployment_type = args.deployment_type
  45. elif 'OS_DEPLOYMENT_TYPE' in os.environ:
  46. deployment_type = os.environ['OS_DEPLOYMENT_TYPE']
  47. return deployment_type
  48. def create(self, args):
  49. """
  50. Create an OpenShift cluster for given provider
  51. :param args: command line arguments provided by user
  52. """
  53. cluster = {'cluster_id': args.cluster_id,
  54. 'deployment_type': self.get_deployment_type(args)}
  55. playbook = "playbooks/{0}/openshift-cluster/launch.yml".format(args.provider)
  56. inventory = self.setup_provider(args.provider)
  57. cluster['num_masters'] = args.masters
  58. cluster['num_nodes'] = args.nodes
  59. cluster['num_infra'] = args.infra
  60. cluster['num_etcd'] = args.etcd
  61. cluster['cluster_env'] = args.env
  62. if args.cloudprovider and args.provider == 'openstack':
  63. cluster['openshift_cloudprovider_kind'] = 'openstack'
  64. cluster['openshift_cloudprovider_openstack_auth_url'] = os.getenv('OS_AUTH_URL')
  65. cluster['openshift_cloudprovider_openstack_username'] = os.getenv('OS_USERNAME')
  66. cluster['openshift_cloudprovider_openstack_password'] = os.getenv('OS_PASSWORD')
  67. cluster['openshift_cloudprovider_openstack_tenant_id'] = os.getenv('OS_PROJECT_ID',os.getenv('OS_TENANT_ID'))
  68. cluster['openshift_cloudprovider_openstack_tenant_name'] = os.getenv('OS_PROJECT_NAME',os.getenv('OS_TENANT_NAME'))
  69. cluster['openshift_cloudprovider_openstack_region'] = os.getenv('OS_REGION_NAME')
  70. self.action(args, inventory, cluster, playbook)
  71. def add_nodes(self, args):
  72. """
  73. Add nodes to an existing cluster for given provider
  74. :param args: command line arguments provided by user
  75. """
  76. cluster = {'cluster_id': args.cluster_id,
  77. 'deployment_type': self.get_deployment_type(args),
  78. }
  79. playbook = "playbooks/{0}/openshift-cluster/add_nodes.yml".format(args.provider)
  80. inventory = self.setup_provider(args.provider)
  81. cluster['num_nodes'] = args.nodes
  82. cluster['num_infra'] = args.infra
  83. cluster['cluster_env'] = args.env
  84. self.action(args, inventory, cluster, playbook)
  85. def terminate(self, args):
  86. """
  87. Destroy OpenShift cluster
  88. :param args: command line arguments provided by user
  89. """
  90. cluster = {'cluster_id': args.cluster_id,
  91. 'deployment_type': self.get_deployment_type(args),
  92. 'cluster_env': args.env,
  93. }
  94. playbook = "playbooks/{0}/openshift-cluster/terminate.yml".format(args.provider)
  95. inventory = self.setup_provider(args.provider)
  96. self.action(args, inventory, cluster, playbook)
  97. def list(self, args):
  98. """
  99. List VMs in cluster
  100. :param args: command line arguments provided by user
  101. """
  102. cluster = {'cluster_id': args.cluster_id,
  103. 'deployment_type': self.get_deployment_type(args),
  104. 'cluster_env': args.env,
  105. }
  106. playbook = "playbooks/{0}/openshift-cluster/list.yml".format(args.provider)
  107. inventory = self.setup_provider(args.provider)
  108. self.action(args, inventory, cluster, playbook)
  109. def config(self, args):
  110. """
  111. Configure or reconfigure OpenShift across clustered VMs
  112. :param args: command line arguments provided by user
  113. """
  114. cluster = {'cluster_id': args.cluster_id,
  115. 'deployment_type': self.get_deployment_type(args),
  116. 'cluster_env': args.env,
  117. }
  118. playbook = "playbooks/{0}/openshift-cluster/config.yml".format(args.provider)
  119. inventory = self.setup_provider(args.provider)
  120. self.action(args, inventory, cluster, playbook)
  121. def update(self, args):
  122. """
  123. Update to latest OpenShift across clustered VMs
  124. :param args: command line arguments provided by user
  125. """
  126. cluster = {'cluster_id': args.cluster_id,
  127. 'deployment_type': self.get_deployment_type(args),
  128. 'cluster_env': args.env,
  129. }
  130. playbook = "playbooks/{0}/openshift-cluster/update.yml".format(args.provider)
  131. inventory = self.setup_provider(args.provider)
  132. self.action(args, inventory, cluster, playbook)
  133. def service(self, args):
  134. """
  135. Make the same service call across all nodes in the cluster
  136. :param args: command line arguments provided by user
  137. """
  138. cluster = {'cluster_id': args.cluster_id,
  139. 'deployment_type': self.get_deployment_type(args),
  140. 'new_cluster_state': args.state,
  141. 'cluster_env': args.env,
  142. }
  143. playbook = "playbooks/{0}/openshift-cluster/service.yml".format(args.provider)
  144. inventory = self.setup_provider(args.provider)
  145. self.action(args, inventory, cluster, playbook)
  146. def setup_provider(self, provider):
  147. """
  148. Setup ansible playbook environment
  149. :param provider: command line arguments provided by user
  150. :return: path to inventory for given provider
  151. """
  152. config = ConfigParser.ConfigParser()
  153. if 'gce' == provider:
  154. gce_ini_default_path = os.path.join('inventory/gce/hosts/gce.ini')
  155. gce_ini_path = os.environ.get('GCE_INI_PATH', gce_ini_default_path)
  156. if os.path.exists(gce_ini_path):
  157. config.readfp(open(gce_ini_path))
  158. for key in config.options('gce'):
  159. os.environ[key] = config.get('gce', key)
  160. inventory = '-i inventory/gce/hosts'
  161. elif 'aws' == provider:
  162. config.readfp(open('inventory/aws/hosts/ec2.ini'))
  163. for key in config.options('ec2'):
  164. os.environ[key] = config.get('ec2', key)
  165. inventory = '-i inventory/aws/hosts'
  166. key_vars = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']
  167. key_missing = [key for key in key_vars if key not in os.environ]
  168. boto_conf_files = ['~/.aws/credentials', '~/.boto']
  169. conf_exists = lambda conf: os.path.isfile(os.path.expanduser(conf))
  170. boto_configs = [conf for conf in boto_conf_files if conf_exists(conf)]
  171. if len(key_missing) > 0 and len(boto_configs) == 0:
  172. raise ValueError("PROVIDER aws requires {0} environment variable(s). See README_AWS.md".format(key_missing))
  173. elif 'libvirt' == provider:
  174. inventory = '-i inventory/libvirt/hosts'
  175. elif 'openstack' == provider:
  176. inventory = '-i inventory/openstack/hosts'
  177. else:
  178. # this code should never be reached
  179. raise ValueError("invalid PROVIDER {0}".format(provider))
  180. return inventory
  181. def action(self, args, inventory, cluster, playbook):
  182. """
  183. Build ansible-playbook command line and execute
  184. :param args: command line arguments provided by user
  185. :param inventory: derived provider library
  186. :param cluster: cluster variables for kubernetes
  187. :param playbook: ansible playbook to execute
  188. """
  189. verbose = ''
  190. if args.verbose > 0:
  191. verbose = '-{0}'.format('v' * args.verbose)
  192. if args.option:
  193. for opt in args.option:
  194. k, v = opt.split('=', 1)
  195. cluster['cli_' + k] = v
  196. ansible_extra_vars = '-e \'{0}\''.format(
  197. ' '.join(['%s=%s' % (key, value) for (key, value) in cluster.items()])
  198. )
  199. command = 'ansible-playbook {0} {1} {2} {3}'.format(
  200. verbose, inventory, ansible_extra_vars, playbook
  201. )
  202. if args.profile:
  203. command = 'ANSIBLE_CALLBACK_PLUGINS=ansible-profile/callback_plugins ' + command
  204. if args.verbose > 1:
  205. command = 'time {0}'.format(command)
  206. if args.verbose > 0:
  207. sys.stderr.write('RUN [{0}]\n'.format(command))
  208. sys.stderr.flush()
  209. try:
  210. subprocess.check_call(command, shell=True)
  211. except subprocess.CalledProcessError as exc:
  212. raise ActionFailed("ACTION [{0}] failed: {1}"
  213. .format(args.action, exc))
  214. class ActionFailed(Exception):
  215. """
  216. Raised when action failed.
  217. """
  218. pass
  219. if __name__ == '__main__':
  220. """
  221. User command to invoke ansible playbooks in a "known" configuration
  222. Reads ~/.openshift-ansible for default configuration items
  223. [DEFAULT]
  224. validate_cluster_ids = False
  225. cluster_ids = marketing,sales
  226. providers = gce,aws,libvirt,openstack
  227. """
  228. warning = ("================================================================================\n"
  229. "ATTENTION: You are running a community supported utility that has not been\n"
  230. "tested by Red Hat. Visit https://docs.openshift.com for supported installation\n"
  231. "instructions.\n"
  232. "================================================================================\n\n")
  233. sys.stderr.write(warning)
  234. cluster_config = ConfigParser.SafeConfigParser({
  235. 'cluster_ids': 'marketing,sales',
  236. 'validate_cluster_ids': 'False',
  237. 'providers': 'gce,aws,libvirt,openstack',
  238. })
  239. path = os.path.expanduser("~/.openshift-ansible")
  240. if os.path.isfile(path):
  241. cluster_config.read(path)
  242. cluster = Cluster()
  243. parser = argparse.ArgumentParser(
  244. formatter_class=argparse.RawDescriptionHelpFormatter,
  245. description='Python wrapper to ensure proper configuration for OpenShift ansible playbooks',
  246. epilog='''\
  247. This wrapper is overriding the following ansible variables:
  248. * ANSIBLE_SSH_ARGS:
  249. If not set in the environment, this wrapper will use the following value:
  250. `-o ForwardAgent=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPersist=600s`
  251. If set in the environment, the environment variable value is left untouched and used.
  252. * ANSIBLE_SSH_PIPELINING:
  253. If not set in the environment, this wrapper will set it to `True`.
  254. If you experience issue with Ansible ssh pipelining, you can disable it by explicitely set this environment variable to `False`.
  255. '''
  256. )
  257. parser.add_argument('-v', '--verbose', action='count',
  258. help='Multiple -v options increase the verbosity')
  259. parser.add_argument('--version', action='version', version='%(prog)s 0.3')
  260. meta_parser = argparse.ArgumentParser(add_help=False)
  261. providers = cluster_config.get('DEFAULT', 'providers').split(',')
  262. meta_parser.add_argument('provider', choices=providers, help='provider')
  263. if cluster_config.get('DEFAULT', 'validate_cluster_ids').lower() in ("yes", "true", "1"):
  264. meta_parser.add_argument('cluster_id', choices=cluster_config.get('DEFAULT', 'cluster_ids').split(','),
  265. help='prefix for cluster VM names')
  266. else:
  267. meta_parser.add_argument('cluster_id', help='prefix for cluster VM names')
  268. meta_parser.add_argument('-t', '--deployment-type',
  269. choices=['origin', 'atomic-enterprise', 'openshift-enterprise'],
  270. help='Deployment type. (default: origin)')
  271. meta_parser.add_argument('-o', '--option', action='append',
  272. help='options')
  273. meta_parser.add_argument('--env', default='dev', type=str,
  274. help='environment for the cluster. Defaults to \'dev\'.')
  275. meta_parser.add_argument('-p', '--profile', action='store_true',
  276. help='Enable playbook profiling')
  277. action_parser = parser.add_subparsers(dest='action', title='actions',
  278. description='Choose from valid actions')
  279. create_parser = action_parser.add_parser('create', help='Create a cluster',
  280. parents=[meta_parser])
  281. create_parser.add_argument('-c', '--cloudprovider', action='store_true',
  282. help='Enable the cloudprovider')
  283. create_parser.add_argument('-m', '--masters', default=1, type=int,
  284. help='number of masters to create in cluster')
  285. create_parser.add_argument('-n', '--nodes', default=2, type=int,
  286. help='number of nodes to create in cluster')
  287. create_parser.add_argument('-i', '--infra', default=1, type=int,
  288. help='number of infra nodes to create in cluster')
  289. create_parser.add_argument('-e', '--etcd', default=0, type=int,
  290. help='number of external etcd hosts to create in cluster')
  291. create_parser.set_defaults(func=cluster.create)
  292. create_parser = action_parser.add_parser('add-nodes', help='Add nodes to a cluster',
  293. parents=[meta_parser])
  294. create_parser.add_argument('-n', '--nodes', default=1, type=int,
  295. help='number of nodes to add to the cluster')
  296. create_parser.add_argument('-i', '--infra', default=1, type=int,
  297. help='number of infra nodes to add to the cluster')
  298. create_parser.set_defaults(func=cluster.add_nodes)
  299. config_parser = action_parser.add_parser('config',
  300. help='Configure or reconfigure a cluster',
  301. parents=[meta_parser])
  302. config_parser.set_defaults(func=cluster.config)
  303. terminate_parser = action_parser.add_parser('terminate',
  304. help='Destroy a cluster',
  305. parents=[meta_parser])
  306. terminate_parser.add_argument('-f', '--force', action='store_true',
  307. help='Destroy cluster without confirmation')
  308. terminate_parser.set_defaults(func=cluster.terminate)
  309. update_parser = action_parser.add_parser('update',
  310. help='Update OpenShift across cluster',
  311. parents=[meta_parser])
  312. update_parser.add_argument('-f', '--force', action='store_true',
  313. help='Update cluster without confirmation')
  314. update_parser.set_defaults(func=cluster.update)
  315. list_parser = action_parser.add_parser('list', help='List VMs in cluster',
  316. parents=[meta_parser])
  317. list_parser.set_defaults(func=cluster.list)
  318. service_parser = action_parser.add_parser('service', help='service for openshift across cluster',
  319. parents=[meta_parser])
  320. # choices are the only ones valid for the ansible service module: http://docs.ansible.com/service_module.html
  321. service_parser.add_argument('state', choices=['started', 'stopped', 'restarted', 'reloaded'],
  322. help='make service call across cluster')
  323. service_parser.set_defaults(func=cluster.service)
  324. args = parser.parse_args()
  325. if 'terminate' == args.action and not args.force:
  326. answer = raw_input("This will destroy the ENTIRE {0} cluster. Are you sure? [y/N] ".format(args.cluster_id))
  327. if answer not in ['y', 'Y']:
  328. sys.stderr.write('\nACTION [terminate] aborted by user!\n')
  329. exit(1)
  330. if 'update' == args.action and not args.force:
  331. answer = raw_input(
  332. "This is destructive and could corrupt {0} cluster. Continue? [y/N] ".format(args.cluster_id))
  333. if answer not in ['y', 'Y']:
  334. sys.stderr.write('\nACTION [update] aborted by user!\n')
  335. exit(1)
  336. try:
  337. args.func(args)
  338. except Exception as exc:
  339. if args.verbose:
  340. traceback.print_exc(file=sys.stderr)
  341. else:
  342. print >>sys.stderr, exc
  343. exit(1)