From 6294264dc4b663381653470935a748a4bb52582f Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 1 Mar 2014 12:56:15 -0800 Subject: [PATCH] Add credential parameters to the GCE modules. In order to simplify the workflow with the GCE modules, it's now possible to add the parameters and project name as arguments to the various GCE modules. The inventory plugin also returns the IP of the host in `ansible_ssh_host` so that you don't have to specify IPs into the inventory file. Some update to the documentation are also added. Closes #5583. --- docsite/rst/intro_dynamic_inventory.rst | 1 + lib/ansible/module_utils/gce.py | 41 ++++++++++++++ library/cloud/gce | 64 +++++++++++----------- library/cloud/gce_lb | 57 ++++++++++---------- library/cloud/gce_net | 66 +++++++++++------------ library/cloud/gce_pd | 71 ++++++++++++------------- plugins/inventory/gce.py | 15 ++++-- 7 files changed, 185 insertions(+), 130 deletions(-) create mode 100644 lib/ansible/module_utils/gce.py mode change 100644 => 100755 library/cloud/gce diff --git a/docsite/rst/intro_dynamic_inventory.rst b/docsite/rst/intro_dynamic_inventory.rst index 32b74603941..83934166a76 100644 --- a/docsite/rst/intro_dynamic_inventory.rst +++ b/docsite/rst/intro_dynamic_inventory.rst @@ -197,6 +197,7 @@ In addition to Cobbler and EC2, inventory scripts are also available for:: BSD Jails Digital Ocean + Google Compute Engine Linode OpenShift OpenStack Nova diff --git a/lib/ansible/module_utils/gce.py b/lib/ansible/module_utils/gce.py new file mode 100644 index 00000000000..f6401c68d01 --- /dev/null +++ b/lib/ansible/module_utils/gce.py @@ -0,0 +1,41 @@ +USER_AGENT_PRODUCT="Ansible-gce" +USER_AGENT_VERSION="v1" + +def gce_connect(module): + """Return a Google Cloud Engine connection.""" + service_account_email = module.params.get('service_account_email', None) + pem_file = module.params.get('pem_file', None) + project_id = module.params.get('project_id', None) + + if service_account_email is None or pem_file is None: + # Load in the libcloud secrets file + try: + import secrets + except ImportError: + secrets = None + + service_account_email, pem_file = getattr(secrets, 'GCE_PARAMS', (None, None)) + keyword_params = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + project_id = keyword_params.get('project', None) + + if service_account_email is None or pem_file is None or project_id is None: + module.fail_json(msg='Missing GCE connection parameters in libcloud secrets file.') + return None + + try: + gce = get_driver(Provider.GCE)(service_account_email, pem_file, datacenter=module.params.get('zone'), project=project_id) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) + except (RuntimeError, ValueError), e: + module.fail_json(msg=str(e), changed=False) + except Exception, e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + return gce + +def unexpected_error_msg(error): + """Create an error string based on passed in error.""" + msg='Unexpected response: HTTP return_code[' + msg+='%s], API error code[%s] and message: %s' % ( + error.http_code, error.code, str(error.value)) + return msg diff --git a/library/cloud/gce b/library/cloud/gce old mode 100644 new mode 100755 index f45377b6cd6..b14ce8996da --- a/library/cloud/gce +++ b/library/cloud/gce @@ -51,6 +51,27 @@ options: required: false default: null aliases: [] + service_account_email: + version_added: 1.5.1 + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: 1.5.1 + description: + - path to the pem file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: 1.5.1 + description: + - your GCE project ID + required: false + default: null + aliases: [] name: description: - identifier when working with a single instance @@ -90,6 +111,8 @@ options: aliases: [] requirements: [ "libcloud" ] +notes: + - Either I(name) or I(instance_names) is required. author: Eric Johnson ''' @@ -119,10 +142,14 @@ EXAMPLES = ''' machine_type: n1-standard-1 image: debian-6 zone: us-central1-a + service_account_email: unique-email@developer.gserviceaccount.com + pem_file: /path/to/pem_file + project_id: project-id tasks: - name: Launch instances local_action: gce instance_names={{names}} machine_type={{machine_type}} - image={{image}} zone={{zone}} + image={{image}} zone={{zone}} service_account_email={{ service_account_email }} + pem_file={{ pem_file }} project_id={{ project_id }} register: gce - name: Wait for SSH to come up local_action: wait_for host={{item.public_ip}} port=22 delay=10 @@ -150,9 +177,6 @@ EXAMPLES = ''' import sys -USER_AGENT_PRODUCT="Ansible-gce" -USER_AGENT_VERSION="v1beta15" - try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver @@ -171,25 +195,6 @@ except ImportError: "msg='GCE module requires python's 'ast' module, python v2.6+'") sys.exit(1) -# Load in the libcloud secrets file -try: - import secrets -except ImportError: - secrets = None -ARGS = getattr(secrets, 'GCE_PARAMS', ()) -KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) - -if not ARGS or not 'project' in KWARGS: - print("failed=True " + \ - "msg='Missing GCE connection parametres in libcloud secrets file.'") - sys.exit(1) - -def unexpected_error_msg(error): - """Create an error string based on passed in error.""" - msg='Unexpected response: HTTP return_code[' - msg+='%s], API error code[%s] and message: %s' % ( - error.http_code, error.code, str(error.value)) - return msg def get_instance_info(inst): """Retrieves instance information from an instance object and returns it @@ -353,9 +358,14 @@ def main(): zone = dict(choices=['us-central1-a', 'us-central1-b', 'us-central2-a', 'europe-west1-a', 'europe-west1-b'], default='us-central1-a'), + service_account_email = dict(), + pem_file = dict(), + project_id = dict(), ) ) + gce = gce_connect(module) + image = module.params.get('image') instance_names = module.params.get('instance_names') machine_type = module.params.get('machine_type') @@ -368,13 +378,6 @@ def main(): zone = module.params.get('zone') changed = False - try: - gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) - gce.connection.user_agent_append("%s/%s" % ( - USER_AGENT_PRODUCT, USER_AGENT_VERSION)) - except Exception, e: - module.fail_json(msg=unexpected_error_msg(e), changed=False) - inames = [] if isinstance(instance_names, list): inames = instance_names @@ -418,5 +421,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.gce import * main() diff --git a/library/cloud/gce_lb b/library/cloud/gce_lb index 5f3cac7d075..3e22c216998 100644 --- a/library/cloud/gce_lb +++ b/library/cloud/gce_lb @@ -110,6 +110,27 @@ options: default: "present" choices: ["active", "present", "absent", "deleted"] aliases: [] + service_account_email: + version_added: 1.5.1 + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: 1.5.1 + description: + - path to the pem file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: 1.5.1 + description: + - your GCE project ID + required: false + default: null + aliases: [] requirements: [ "libcloud" ] author: Eric Johnson @@ -129,41 +150,20 @@ EXAMPLES = ''' import sys -USER_AGENT_PRODUCT="Ansible-gce_lb" -USER_AGENT_VERSION="v1beta15" -try: +try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver from libcloud.loadbalancer.types import Provider as Provider_lb from libcloud.loadbalancer.providers import get_driver as get_driver_lb from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ ResourceExistsError, ResourceNotFoundError - _ = Provider.GCE -except ImportError: + _ = Provider.GCE +except ImportError: print("failed=True " + \ "msg='libcloud with GCE support required for this module.'") - sys.exit(1) - -# Load in the libcloud secrets file -try: - import secrets -except ImportError: - secrets = None -ARGS = getattr(secrets, 'GCE_PARAMS', ()) -KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) - -if not ARGS or not 'project' in KWARGS: - print("failed=True msg='Missing GCE connection " + \ - "parameters in libcloud secrets file.'") sys.exit(1) -def unexpected_error_msg(error): - """Format error string based on passed in error.""" - msg='Unexpected response: HTTP return_code[' - msg+='%s], API error code[%s] and message: %s' % ( - error.http_code, error.code, str(error.value)) - return msg def main(): module = AnsibleModule( @@ -183,9 +183,14 @@ def main(): port_range = dict(), members = dict(type='list'), state = dict(default='present'), + service_account_email = dict(), + pem_file = dict(), + project_id = dict(), ) ) + gce = gce_connect(module) + httphealthcheck_name = module.params.get('httphealthcheck_name') httphealthcheck_port = module.params.get('httphealthcheck_port') httphealthcheck_path = module.params.get('httphealthcheck_path') @@ -205,9 +210,6 @@ def main(): state = module.params.get('state') try: - gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) - gce.connection.user_agent_append("%s/%s" % ( - USER_AGENT_PRODUCT, USER_AGENT_VERSION)) gcelb = get_driver_lb(Provider_lb.GCE)(gce_driver=gce) gcelb.connection.user_agent_append("%s/%s" % ( USER_AGENT_PRODUCT, USER_AGENT_VERSION)) @@ -329,5 +331,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.gce import * main() diff --git a/library/cloud/gce_net b/library/cloud/gce_net index de272ea3825..4e731f196d3 100644 --- a/library/cloud/gce_net +++ b/library/cloud/gce_net @@ -73,6 +73,27 @@ options: default: "present" choices: ["active", "present", "absent", "deleted"] aliases: [] + service_account_email: + version_added: 1.5.1 + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: 1.5.1 + description: + - path to the pem file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: 1.5.1 + description: + - your GCE project ID + required: false + default: null + aliases: [] requirements: [ "libcloud" ] author: Eric Johnson @@ -96,39 +117,17 @@ EXAMPLES = ''' import sys -USER_AGENT_PRODUCT="Ansible-gce_net" -USER_AGENT_VERSION="v1beta15" - -try: - from libcloud.compute.types import Provider - from libcloud.compute.providers import get_driver +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ ResourceExistsError, ResourceNotFoundError - _ = Provider.GCE -except ImportError: + _ = Provider.GCE +except ImportError: print("failed=True " + \ "msg='libcloud with GCE support required for this module.'") - sys.exit(1) - -# Load in the libcloud secrets file -try: - import secrets -except ImportError: - secrets = None -ARGS = getattr(secrets, 'GCE_PARAMS', ()) -KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) - -if not ARGS or not 'project' in KWARGS: - print("failed=True msg='Missing GCE connection " + \ - "parameters in libcloud secrets file.'") sys.exit(1) -def unexpected_error_msg(error): - """Format error string based on passed in error.""" - msg='Unexpected response: HTTP return_code[' - msg+='%s], API error code[%s] and message: %s' % ( - error.http_code, error.code, str(error.value)) - return msg def format_allowed(allowed): """Format the 'allowed' value so that it is GCE compatible.""" @@ -159,9 +158,14 @@ def main(): src_range = dict(), src_tags = dict(type='list'), state = dict(default='present'), + service_account_email = dict(), + pem_file = dict(), + project_id = dict(), ) ) + gce = gce_connect(module) + allowed = module.params.get('allowed') ipv4_range = module.params.get('ipv4_range') fwname = module.params.get('fwname') @@ -170,13 +174,6 @@ def main(): src_tags = module.params.get('src_tags') state = module.params.get('state') - try: - gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) - gce.connection.user_agent_append("%s/%s" % ( - USER_AGENT_PRODUCT, USER_AGENT_VERSION)) - except Exception, e: - module.fail_json(msg=unexpected_error_msg(e), changed=False) - changed = False json_output = {'state': state} @@ -269,5 +266,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.gce import * main() diff --git a/library/cloud/gce_pd b/library/cloud/gce_pd index 6bd71eb3f37..a8e631a5522 100644 --- a/library/cloud/gce_pd +++ b/library/cloud/gce_pd @@ -75,6 +75,27 @@ options: required: false default: "us-central1-b" aliases: [] + service_account_email: + version_added: 1.5.1 + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: 1.5.1 + description: + - path to the pem file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: 1.5.1 + description: + - your GCE project ID + required: false + default: null + aliases: [] requirements: [ "libcloud" ] author: Eric Johnson @@ -82,47 +103,26 @@ author: Eric Johnson EXAMPLES = ''' # Simple attachment action to an existing instance -- local_action: - module: gce_pd +- local_action: + module: gce_pd instance_name: notlocalhost - size_gb: 5 + size_gb: 5 name: pd ''' import sys -USER_AGENT_PRODUCT="Ansible-gce_pd" -USER_AGENT_VERSION="v1beta15" - -try: - from libcloud.compute.types import Provider - from libcloud.compute.providers import get_driver +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ ResourceExistsError, ResourceNotFoundError, ResourceInUseError - _ = Provider.GCE -except ImportError: + _ = Provider.GCE +except ImportError: print("failed=True " + \ "msg='libcloud with GCE support is required for this module.'") - sys.exit(1) - -# Load in the libcloud secrets file -try: - import secrets -except ImportError: - secrets = None -ARGS = getattr(secrets, 'GCE_PARAMS', ()) -KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) - -if not ARGS or not 'project' in KWARGS: - print("failed=True " + \ - "msg='Missing GCE connection parameters in libcloud secrets file.'") sys.exit(1) -def unexpected_error_msg(error): - msg='Unexpected response: HTTP return_code[' - msg+='%s], API error code[%s] and message: %s' % ( - error.http_code, error.code, str(error.value)) - return msg def main(): module = AnsibleModule( @@ -135,9 +135,14 @@ def main(): size_gb = dict(default=10), state = dict(default='present'), zone = dict(default='us-central1-b'), + service_account_email = dict(), + pem_file = dict(), + project_id = dict(), ) ) + gce = gce_connect(module) + detach_only = module.params.get('detach_only') instance_name = module.params.get('instance_name') mode = module.params.get('mode') @@ -151,13 +156,6 @@ def main(): msg='Must specify an instance name when detaching a disk', changed=False) - try: - gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) - gce.connection.user_agent_append("%s/%s" % ( - USER_AGENT_PRODUCT, USER_AGENT_VERSION)) - except Exception, e: - module.fail_json(msg=unexpected_error_msg(e), changed=False) - disk = inst = None changed = is_attached = False @@ -251,5 +249,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.gce import * main() diff --git a/plugins/inventory/gce.py b/plugins/inventory/gce.py index 22082a1e65f..6757e87b55c 100755 --- a/plugins/inventory/gce.py +++ b/plugins/inventory/gce.py @@ -73,7 +73,7 @@ Version: 0.0.1 ''' USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin" -USER_AGENT_VERSION="v1beta15" +USER_AGENT_VERSION="v1" import sys import os @@ -174,6 +174,10 @@ class GceInventory(object): def node_to_dict(self, inst): md = {} + + if inst is None: + return {} + if inst.extra['metadata'].has_key('items'): for entry in inst.extra['metadata']['items']: md[entry['key']] = entry['value'] @@ -192,12 +196,17 @@ class GceInventory(object): 'gce_zone': inst.extra['zone'].name, 'gce_tags': inst.extra['tags'], 'gce_metadata': md, - 'gce_network': net + 'gce_network': net, + # Hosts don't have a public name, so we add an IP + 'ansible_ssh_host': inst.public_ips[0] } def get_instance(self, instance_name): '''Gets details about a specific instance ''' - return self.driver.ex_get_node(instance_name) + try: + return self.driver.ex_get_node(instance_name) + except Exception, e: + return None def group_instances(self): '''Group all instances'''