gce.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/usr/bin/python
  2. # Copyright 2013 Google Inc.
  3. #
  4. # This file is part of Ansible
  5. #
  6. # Ansible is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU 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. # Ansible 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 General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  18. DOCUMENTATION = '''
  19. ---
  20. module: gce
  21. version_added: "1.4"
  22. short_description: create or terminate GCE instances
  23. description:
  24. - Creates or terminates Google Compute Engine (GCE) instances. See
  25. U(https://cloud.google.com/products/compute-engine) for an overview.
  26. Full install/configuration instructions for the gce* modules can
  27. be found in the comments of ansible/test/gce_tests.py.
  28. options:
  29. image:
  30. description:
  31. - image string to use for the instance
  32. required: false
  33. default: "debian-7"
  34. instance_names:
  35. description:
  36. - a comma-separated list of instance names to create or destroy
  37. required: false
  38. default: null
  39. machine_type:
  40. description:
  41. - machine type to use for the instance, use 'n1-standard-1' by default
  42. required: false
  43. default: "n1-standard-1"
  44. metadata:
  45. description:
  46. - a hash/dictionary of custom data for the instance;
  47. '{"key":"value", ...}'
  48. required: false
  49. default: null
  50. service_account_email:
  51. version_added: "1.5.1"
  52. description:
  53. - service account email
  54. required: false
  55. default: null
  56. service_account_permissions:
  57. version_added: "2.0"
  58. description:
  59. - service account permissions (see
  60. U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create),
  61. --scopes section for detailed information)
  62. required: false
  63. default: null
  64. choices: [
  65. "bigquery", "cloud-platform", "compute-ro", "compute-rw",
  66. "computeaccounts-ro", "computeaccounts-rw", "datastore", "logging-write",
  67. "monitoring", "sql", "sql-admin", "storage-full", "storage-ro",
  68. "storage-rw", "taskqueue", "userinfo-email"
  69. ]
  70. pem_file:
  71. version_added: "1.5.1"
  72. description:
  73. - path to the pem file associated with the service account email
  74. required: false
  75. default: null
  76. project_id:
  77. version_added: "1.5.1"
  78. description:
  79. - your GCE project ID
  80. required: false
  81. default: null
  82. name:
  83. description:
  84. - identifier when working with a single instance
  85. required: false
  86. network:
  87. description:
  88. - name of the network, 'default' will be used if not specified
  89. required: false
  90. default: "default"
  91. persistent_boot_disk:
  92. description:
  93. - if set, create the instance with a persistent boot disk
  94. required: false
  95. default: "false"
  96. disks:
  97. description:
  98. - a list of persistent disks to attach to the instance; a string value
  99. gives the name of the disk; alternatively, a dictionary value can
  100. define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry
  101. will be the boot disk (which must be READ_WRITE).
  102. required: false
  103. default: null
  104. version_added: "1.7"
  105. state:
  106. description:
  107. - desired state of the resource
  108. required: false
  109. default: "present"
  110. choices: ["active", "present", "absent", "deleted"]
  111. tags:
  112. description:
  113. - a comma-separated list of tags to associate with the instance
  114. required: false
  115. default: null
  116. zone:
  117. description:
  118. - the GCE zone to use
  119. required: true
  120. default: "us-central1-a"
  121. ip_forward:
  122. version_added: "1.9"
  123. description:
  124. - set to true if the instance can forward ip packets (useful for
  125. gateways)
  126. required: false
  127. default: "false"
  128. external_ip:
  129. version_added: "1.9"
  130. description:
  131. - type of external ip, ephemeral by default
  132. required: false
  133. default: "ephemeral"
  134. disk_auto_delete:
  135. version_added: "1.9"
  136. description:
  137. - if set boot disk will be removed after instance destruction
  138. required: false
  139. default: "true"
  140. requirements:
  141. - "python >= 2.6"
  142. - "apache-libcloud >= 0.13.3"
  143. notes:
  144. - Either I(name) or I(instance_names) is required.
  145. author: "Eric Johnson (@erjohnso) <erjohnso@google.com>"
  146. '''
  147. EXAMPLES = '''
  148. # Basic provisioning example. Create a single Debian 7 instance in the
  149. # us-central1-a Zone of n1-standard-1 machine type.
  150. - local_action:
  151. module: gce
  152. name: test-instance
  153. zone: us-central1-a
  154. machine_type: n1-standard-1
  155. image: debian-7
  156. # Example using defaults and with metadata to create a single 'foo' instance
  157. - local_action:
  158. module: gce
  159. name: foo
  160. metadata: '{"db":"postgres", "group":"qa", "id":500}'
  161. # Launch instances from a control node, runs some tasks on the new instances,
  162. # and then terminate them
  163. - name: Create a sandbox instance
  164. hosts: localhost
  165. vars:
  166. names: foo,bar
  167. machine_type: n1-standard-1
  168. image: debian-6
  169. zone: us-central1-a
  170. service_account_email: unique-email@developer.gserviceaccount.com
  171. pem_file: /path/to/pem_file
  172. project_id: project-id
  173. tasks:
  174. - name: Launch instances
  175. local_action: gce instance_names={{names}} machine_type={{machine_type}}
  176. image={{image}} zone={{zone}}
  177. service_account_email={{ service_account_email }}
  178. pem_file={{ pem_file }} project_id={{ project_id }}
  179. register: gce
  180. - name: Wait for SSH to come up
  181. local_action: wait_for host={{item.public_ip}} port=22 delay=10
  182. timeout=60 state=started
  183. with_items: {{gce.instance_data}}
  184. - name: Configure instance(s)
  185. hosts: launched
  186. sudo: True
  187. roles:
  188. - my_awesome_role
  189. - my_awesome_tasks
  190. - name: Terminate instances
  191. hosts: localhost
  192. connection: local
  193. tasks:
  194. - name: Terminate instances that were previously launched
  195. local_action:
  196. module: gce
  197. state: 'absent'
  198. instance_names: {{gce.instance_names}}
  199. '''
  200. try:
  201. import libcloud
  202. from libcloud.compute.types import Provider
  203. from libcloud.compute.providers import get_driver
  204. from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
  205. ResourceExistsError, ResourceInUseError, ResourceNotFoundError
  206. _ = Provider.GCE
  207. HAS_LIBCLOUD = True
  208. except ImportError:
  209. HAS_LIBCLOUD = False
  210. try:
  211. from ast import literal_eval
  212. HAS_PYTHON26 = True
  213. except ImportError:
  214. HAS_PYTHON26 = False
  215. def get_instance_info(inst):
  216. """Retrieves instance information from an instance object and returns it
  217. as a dictionary.
  218. """
  219. metadata = {}
  220. if 'metadata' in inst.extra and 'items' in inst.extra['metadata']:
  221. for md in inst.extra['metadata']['items']:
  222. metadata[md['key']] = md['value']
  223. try:
  224. netname = inst.extra['networkInterfaces'][0]['network'].split('/')[-1]
  225. except:
  226. netname = None
  227. if 'disks' in inst.extra:
  228. disk_names = [disk_info['source'].split('/')[-1]
  229. for disk_info
  230. in sorted(inst.extra['disks'],
  231. key=lambda disk_info: disk_info['index'])]
  232. else:
  233. disk_names = []
  234. if len(inst.public_ips) == 0:
  235. public_ip = None
  236. else:
  237. public_ip = inst.public_ips[0]
  238. return({
  239. 'image': inst.image is not None and inst.image.split('/')[-1] or None,
  240. 'disks': disk_names,
  241. 'machine_type': inst.size,
  242. 'metadata': metadata,
  243. 'name': inst.name,
  244. 'network': netname,
  245. 'private_ip': inst.private_ips[0],
  246. 'public_ip': public_ip,
  247. 'status': ('status' in inst.extra) and inst.extra['status'] or None,
  248. 'tags': ('tags' in inst.extra) and inst.extra['tags'] or [],
  249. 'zone': ('zone' in inst.extra) and inst.extra['zone'].name or None,
  250. })
  251. def create_instances(module, gce, instance_names):
  252. """Creates new instances. Attributes other than instance_names are picked
  253. up from 'module'
  254. module : AnsibleModule object
  255. gce: authenticated GCE libcloud driver
  256. instance_names: python list of instance names to create
  257. Returns:
  258. A list of dictionaries with instance information
  259. about the instances that were launched.
  260. """
  261. image = module.params.get('image')
  262. machine_type = module.params.get('machine_type')
  263. metadata = module.params.get('metadata')
  264. network = module.params.get('network')
  265. persistent_boot_disk = module.params.get('persistent_boot_disk')
  266. disks = module.params.get('disks')
  267. state = module.params.get('state')
  268. tags = module.params.get('tags')
  269. zone = module.params.get('zone')
  270. ip_forward = module.params.get('ip_forward')
  271. external_ip = module.params.get('external_ip')
  272. disk_auto_delete = module.params.get('disk_auto_delete')
  273. service_account_permissions = module.params.get('service_account_permissions')
  274. service_account_email = module.params.get('service_account_email')
  275. if external_ip == "none":
  276. external_ip = None
  277. new_instances = []
  278. changed = False
  279. lc_image = gce.ex_get_image(image)
  280. lc_disks = []
  281. disk_modes = []
  282. for i, disk in enumerate(disks or []):
  283. if isinstance(disk, dict):
  284. lc_disks.append(gce.ex_get_volume(disk['name']))
  285. disk_modes.append(disk['mode'])
  286. else:
  287. lc_disks.append(gce.ex_get_volume(disk))
  288. # boot disk is implicitly READ_WRITE
  289. disk_modes.append('READ_ONLY' if i > 0 else 'READ_WRITE')
  290. lc_network = gce.ex_get_network(network)
  291. lc_machine_type = gce.ex_get_size(machine_type)
  292. lc_zone = gce.ex_get_zone(zone)
  293. # Try to convert the user's metadata value into the format expected
  294. # by GCE. First try to ensure user has proper quoting of a
  295. # dictionary-like syntax using 'literal_eval', then convert the python
  296. # dict into a python list of 'key' / 'value' dicts. Should end up
  297. # with:
  298. # [ {'key': key1, 'value': value1}, {'key': key2, 'value': value2}, ...]
  299. if metadata:
  300. if isinstance(metadata, dict):
  301. md = metadata
  302. else:
  303. try:
  304. md = literal_eval(str(metadata))
  305. if not isinstance(md, dict):
  306. raise ValueError('metadata must be a dict')
  307. except ValueError as e:
  308. module.fail_json(msg='bad metadata: %s' % str(e))
  309. except SyntaxError as e:
  310. module.fail_json(msg='bad metadata syntax')
  311. if hasattr(libcloud, '__version__') and libcloud.__version__ < '0.15':
  312. items = []
  313. for k, v in md.items():
  314. items.append({"key": k, "value": v})
  315. metadata = {'items': items}
  316. else:
  317. metadata = md
  318. ex_sa_perms = []
  319. bad_perms = []
  320. if service_account_permissions:
  321. for perm in service_account_permissions:
  322. if perm not in gce.SA_SCOPES_MAP.keys():
  323. bad_perms.append(perm)
  324. if len(bad_perms) > 0:
  325. module.fail_json(msg='bad permissions: %s' % str(bad_perms))
  326. if service_account_email:
  327. ex_sa_perms.append({'email': service_account_email})
  328. else:
  329. ex_sa_perms.append({'email': "default"})
  330. ex_sa_perms[0]['scopes'] = service_account_permissions
  331. # These variables all have default values but check just in case
  332. if not lc_image or not lc_network or not lc_machine_type or not lc_zone:
  333. module.fail_json(msg='Missing required create instance variable',
  334. changed=False)
  335. for name in instance_names:
  336. pd = None
  337. if lc_disks:
  338. pd = lc_disks[0]
  339. elif persistent_boot_disk:
  340. try:
  341. pd = gce.create_volume(None, "%s" % name, image=lc_image)
  342. except ResourceExistsError:
  343. pd = gce.ex_get_volume("%s" % name, lc_zone)
  344. inst = None
  345. try:
  346. inst = gce.create_node(
  347. name, lc_machine_type, lc_image, location=lc_zone,
  348. ex_network=network, ex_tags=tags, ex_metadata=metadata,
  349. ex_boot_disk=pd, ex_can_ip_forward=ip_forward,
  350. external_ip=external_ip, ex_disk_auto_delete=disk_auto_delete,
  351. ex_service_accounts=ex_sa_perms
  352. )
  353. changed = True
  354. except ResourceExistsError:
  355. inst = gce.ex_get_node(name, lc_zone)
  356. except GoogleBaseError as e:
  357. module.fail_json(msg='Unexpected error attempting to create ' +
  358. 'instance %s, error: %s' % (name, e.value))
  359. for i, lc_disk in enumerate(lc_disks):
  360. # Check whether the disk is already attached
  361. if (len(inst.extra['disks']) > i):
  362. attached_disk = inst.extra['disks'][i]
  363. if attached_disk['source'] != lc_disk.extra['selfLink']:
  364. module.fail_json(
  365. msg=("Disk at index %d does not match: requested=%s found=%s" % (
  366. i, lc_disk.extra['selfLink'], attached_disk['source'])))
  367. elif attached_disk['mode'] != disk_modes[i]:
  368. module.fail_json(
  369. msg=("Disk at index %d is in the wrong mode: requested=%s found=%s" % (
  370. i, disk_modes[i], attached_disk['mode'])))
  371. else:
  372. continue
  373. gce.attach_volume(inst, lc_disk, ex_mode=disk_modes[i])
  374. # Work around libcloud bug: attached volumes don't get added
  375. # to the instance metadata. get_instance_info() only cares about
  376. # source and index.
  377. if len(inst.extra['disks']) != i+1:
  378. inst.extra['disks'].append(
  379. {'source': lc_disk.extra['selfLink'], 'index': i})
  380. if inst:
  381. new_instances.append(inst)
  382. instance_names = []
  383. instance_json_data = []
  384. for inst in new_instances:
  385. d = get_instance_info(inst)
  386. instance_names.append(d['name'])
  387. instance_json_data.append(d)
  388. return (changed, instance_json_data, instance_names)
  389. def terminate_instances(module, gce, instance_names, zone_name):
  390. """Terminates a list of instances.
  391. module: Ansible module object
  392. gce: authenticated GCE connection object
  393. instance_names: a list of instance names to terminate
  394. zone_name: the zone where the instances reside prior to termination
  395. Returns a dictionary of instance names that were terminated.
  396. """
  397. changed = False
  398. terminated_instance_names = []
  399. for name in instance_names:
  400. inst = None
  401. try:
  402. inst = gce.ex_get_node(name, zone_name)
  403. except ResourceNotFoundError:
  404. pass
  405. except Exception as e:
  406. module.fail_json(msg=unexpected_error_msg(e), changed=False)
  407. if inst:
  408. gce.destroy_node(inst)
  409. terminated_instance_names.append(inst.name)
  410. changed = True
  411. return (changed, terminated_instance_names)
  412. def main():
  413. module = AnsibleModule(
  414. argument_spec=dict(
  415. image=dict(default='debian-7'),
  416. instance_names=dict(),
  417. machine_type=dict(default='n1-standard-1'),
  418. metadata=dict(),
  419. name=dict(),
  420. network=dict(default='default'),
  421. persistent_boot_disk=dict(type='bool', default=False),
  422. disks=dict(type='list'),
  423. state=dict(choices=['active', 'present', 'absent', 'deleted'],
  424. default='present'),
  425. tags=dict(type='list'),
  426. zone=dict(default='us-central1-a'),
  427. service_account_email=dict(),
  428. service_account_permissions=dict(type='list'),
  429. pem_file=dict(),
  430. project_id=dict(),
  431. ip_forward=dict(type='bool', default=False),
  432. external_ip=dict(choices=['ephemeral', 'none'],
  433. default='ephemeral'),
  434. disk_auto_delete=dict(type='bool', default=True),
  435. )
  436. )
  437. if not HAS_PYTHON26:
  438. module.fail_json(msg="GCE module requires python's 'ast' module, python v2.6+")
  439. if not HAS_LIBCLOUD:
  440. module.fail_json(msg='libcloud with GCE support (0.13.3+) required for this module')
  441. gce = gce_connect(module)
  442. image = module.params.get('image')
  443. instance_names = module.params.get('instance_names')
  444. machine_type = module.params.get('machine_type')
  445. metadata = module.params.get('metadata')
  446. name = module.params.get('name')
  447. network = module.params.get('network')
  448. persistent_boot_disk = module.params.get('persistent_boot_disk')
  449. state = module.params.get('state')
  450. tags = module.params.get('tags')
  451. zone = module.params.get('zone')
  452. ip_forward = module.params.get('ip_forward')
  453. changed = False
  454. inames = []
  455. if isinstance(instance_names, list):
  456. inames = instance_names
  457. elif isinstance(instance_names, str):
  458. inames = instance_names.split(',')
  459. if name:
  460. inames.append(name)
  461. if not inames:
  462. module.fail_json(msg='Must specify a "name" or "instance_names"',
  463. changed=False)
  464. if not zone:
  465. module.fail_json(msg='Must specify a "zone"', changed=False)
  466. json_output = {'zone': zone}
  467. if state in ['absent', 'deleted']:
  468. json_output['state'] = 'absent'
  469. (changed, terminated_instance_names) = terminate_instances(
  470. module, gce, inames, zone)
  471. # based on what user specified, return the same variable, although
  472. # value could be different if an instance could not be destroyed
  473. if instance_names:
  474. json_output['instance_names'] = terminated_instance_names
  475. elif name:
  476. json_output['name'] = name
  477. elif state in ['active', 'present']:
  478. json_output['state'] = 'present'
  479. (changed, instance_data, instance_name_list) = create_instances(
  480. module, gce, inames)
  481. json_output['instance_data'] = instance_data
  482. if instance_names:
  483. json_output['instance_names'] = instance_name_list
  484. elif name:
  485. json_output['name'] = name
  486. json_output['changed'] = changed
  487. module.exit_json(**json_output)
  488. # import module snippets
  489. from ansible.module_utils.basic import *
  490. from ansible.module_utils.gce import *
  491. if __name__ == '__main__':
  492. main()