1    	#!@PYTHON@ -tt
2    	
3    	#
4    	# Requires the googleapiclient and oauth2client
5    	# RHEL 7.x: google-api-python-client==1.6.7 python-gflags==2.0 pyasn1==0.4.8 rsa==3.4.2 pysocks==1.7.1 httplib2==0.19.0
6    	# RHEL 8.x: pysocks==1.7.1 httplib2==0.19.0
7    	# SLES 12.x: python-google-api-python-client python-oauth2client python-oauth2client-gce pysocks==1.7.1 httplib2==0.19.0
8    	# SLES 15.x: python3-google-api-python-client python3-oauth2client pysocks==1.7.1 httplib2==0.19.0
9    	#
10   	
11   	import atexit
12   	import logging
13   	import json
14   	import re
15   	import os
16   	import socket
17   	import sys
18   	import time
19   	
20   	from ssl import SSLError
21   	
22   	if sys.version_info >= (3, 0):
23   	  # Python 3 imports.
24   	  import urllib.parse as urlparse
25   	  import urllib.request as urlrequest
26   	else:
27   	  # Python 2 imports.
28   	  import urllib as urlparse
29   	  import urllib2 as urlrequest
30   	sys.path.append("@FENCEAGENTSLIBDIR@")
31   	
32   	from fencing import fail_usage, run_delay, all_opt, atexit_handler, check_input, process_input, show_docs, fence_action, run_command
33   	try:
34   	  import httplib2
35   	  import googleapiclient.discovery
36   	  try:
37   	    from google.oauth2.credentials import Credentials as GoogleCredentials
38   	  except ImportError:
39   	    from oauth2client.client import GoogleCredentials
40   	except Exception as e:
41   	    from fencing import fail_import_if_not_metadata_or_help_action
42   	    fail_import_if_not_metadata_or_help_action("Failed to import Google Cloud dependencies", e)
43   	
44   	VERSION = '1.0.5'
45   	ACTION_IDS = {
46   			'on': 1, 'off': 2, 'reboot': 3, 'status': 4, 'list': 5, 'list-status': 6,
47   			'monitor': 7, 'metadata': 8, 'manpage': 9, 'validate-all': 10
48   	}
49   	USER_AGENT = 'sap-core-eng/fencegce/%s/%s/ACTION/%s'
50   	METADATA_SERVER = 'http://metadata.google.internal/computeMetadata/v1/'
51   	METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
52   	INSTANCE_LINK = 'https://www.googleapis.com/compute/v1/projects/{}/zones/{}/instances/{}'
53   	DEFAULT_UNIVERSE_DOMAIN = 'googleapis.com'
54   	UNIVERSE_DOMAIN_ENV = 'GOOGLE_CLOUD_UNIVERSE_DOMAIN'
55   	
56   	def run_on_fail(options):
57   		if "--runonfail" in options:
58   			run_command(options, options["--runonfail"])
59   	
60   	def fail_fence_agent(options, message):
61   		run_on_fail(options)
62   		fail_usage(message)
63   	
64   	def raise_fence_agent(options, message):
65   		run_on_fail(options)
66   		raise Exception(message)
67   	
68   	#
69   	# The universe domain is googleapis.com unless running in another Google Cloud
70   	# universe, e.g. Trusted Partner Cloud. Uses the GOOGLE_CLOUD_UNIVERSE_DOMAIN
71   	# environment variable, then the metadata server (except on bare metal).
72   	#
73   	def get_universe_domain(options):
74   		universe_domain = os.environ.get(UNIVERSE_DOMAIN_ENV, "").strip().rstrip(".").lower()
75   		if not universe_domain and "--baremetalsolution" not in options:
76   			try:
77   				universe_domain = get_metadata('universe/universe-domain', timeout=2).strip().rstrip(".").lower()
78   			except Exception as err:
79   				logging.debug("Failed retrieving the universe domain: %s" % str(err))
80   		return universe_domain or DEFAULT_UNIVERSE_DOMAIN
81   	
82   	#
83   	# Rewrites *.googleapis.com uris to the universe domain, for client libraries
84   	# that do not support universe domains.
85   	#
86   	def universe_api_uri(options, uri):
87   		universe_domain = options.get("universe_domain", DEFAULT_UNIVERSE_DOMAIN)
88   		if universe_domain == DEFAULT_UNIVERSE_DOMAIN:
89   			return uri
90   		# Compute Engine resource links use www.googleapis.com/compute
91   		uri = re.sub(r"^https://www\.googleapis\.com/compute/",
92   			lambda m: "https://compute.%s/compute/" % universe_domain, uri)
93   		return re.sub(r"^https://([a-z0-9-]+)\.googleapis\.com/",
94   			lambda m: "https://%s.%s/" % (m.group(1), universe_domain), uri)
95   	
96   	#
97   	# Will use baremetalsolution setting or the environment variable
98   	# FENCE_GCE_URI_REPLACEMENTS to replace the uri for calls to *.googleapis.com.
99   	#
100  	def replace_api_uri(options, http_request):
101  		if http_request is None:
102  			return None
103  		uri_replacements = []
104  		# put any env var replacements first, then baremetalsolution if in options
105  		if "FENCE_GCE_URI_REPLACEMENTS" in os.environ:
106  			logging.debug("FENCE_GCE_URI_REPLACEMENTS environment variable exists")
107  			env_uri_replacements = os.environ["FENCE_GCE_URI_REPLACEMENTS"]
108  			try:
109  				uri_replacements_json = json.loads(env_uri_replacements)
110  				if isinstance(uri_replacements_json, list):
111  					uri_replacements = uri_replacements_json
112  				else:
113  					logging.warning("FENCE_GCE_URI_REPLACEMENTS exists, but is not a JSON List")
114  			except ValueError as e:
115  				logging.warning("FENCE_GCE_URI_REPLACEMENTS exists but is not valid JSON")
116  		if "--baremetalsolution" in options:
117  			uri_replacements.append(
118  				{
119  					"matchlength": 4,
120  					"match": r"https://compute.googleapis.com/compute/v1/projects/(.*)/zones/(.*)/instances/(.*)/reset(.*)",
121  					"replace": r"https://baremetalsolution.googleapis.com/v1/projects/\1/locations/\2/instances/\3:resetInstance\4"
122  				})
123  		for uri_replacement in uri_replacements:
124  			# each uri_replacement should have matchlength, match, and replace
125  			if "matchlength" not in uri_replacement or "match" not in uri_replacement or "replace" not in uri_replacement:
126  				logging.warning("FENCE_GCE_URI_REPLACEMENTS missing matchlength, match, or replace in %s" % uri_replacement)
127  				continue
128  			match = re.match(uri_replacement["match"], http_request.uri)
129  			if match is None or len(match.groups()) != uri_replacement["matchlength"]:
130  				continue
131  			replaced_uri = re.sub(uri_replacement["match"], uri_replacement["replace"], http_request.uri)
132  			match = re.match(r"https:\/\/.*.googleapis.com", replaced_uri)
133  			if match is None or match.start() != 0:
134  				logging.warning("FENCE_GCE_URI_REPLACEMENTS replace is not "
135  					"targeting googleapis.com, ignoring it: %s" % replaced_uri)
136  				continue
137  			logging.debug("Replacing googleapis uri %s with %s" % (http_request.uri, replaced_uri))
138  			http_request.uri = replaced_uri
139  			break
140  		http_request.uri = universe_api_uri(options, http_request.uri)
141  		return http_request
142  	
143  	def retry_api_execute(options, http_request):
144  		replaced_http_request = replace_api_uri(options, http_request)
145  		action = ACTION_IDS[options["--action"]] if options["--action"] in ACTION_IDS else 0
146  		try:
147  			user_agent_header = USER_AGENT % (VERSION, options["image"], action)
148  		except ValueError:
149  			user_agent_header = USER_AGENT % (VERSION, options["image"], 0)
150  		replaced_http_request.headers["User-Agent"] = user_agent_header
151  		logging.debug("User agent set as %s" % (user_agent_header))
152  		retries = 3
153  		if options.get("--retries"):
154  			retries = int(options.get("--retries"))
155  		retry_sleep = 5
156  		if options.get("--retrysleep"):
157  			retry_sleep = int(options.get("--retrysleep"))
158  		retry = 0
159  		current_err = None
160  		while retry <= retries:
161  			if retry > 0:
162  				time.sleep(retry_sleep)
163  			try:
164  				return replaced_http_request.execute()
165  			except Exception as err:
166  				current_err = err
167  				logging.warning("Could not execute api call to: %s, retry: %s, "
168  					"err: %s" % (replaced_http_request.uri, retry, str(err)))
169  			retry += 1
170  		raise current_err
171  	
172  	
173  	def translate_status(instance_status):
174  		"Returns on | off | unknown."
175  		if instance_status == "RUNNING":
176  			return "on"
177  		elif instance_status == "TERMINATED":
178  			return "off"
179  		return "unknown"
180  	
181  	
182  	def get_nodes_list(conn, options):
183  		result = {}
184  		plug = options["--plug"] if "--plug" in options else ""
185  		zones = options["--zone"] if "--zone" in options else ""
186  		filter = "name="+plug if plug != "" else ""
187  		max_results = 1 if options.get("--action") == "monitor" else 500
188  		if not zones:
189  			zones = get_zone(conn, options, plug) if "--plugzonemap" not in options else options["--plugzonemap"][plug]
190  		try:
191  			for zone in zones.split(","):
192  				request = conn.instances().list(
193  					project=options["--project"],
194  					zone=zone,
195  					filter=filter,
196  					maxResults=max_results)
197  			while request is not None:
198  				instanceList = retry_api_execute(options, request)
199  				if "items" not in instanceList:
200  					break
201  				for instance in instanceList["items"]:
202  					result[instance["id"]] = (instance["name"], translate_status(instance["status"]))
203  				request = conn.instances().list_next(previous_request=request, previous_response=instanceList)
204  		except Exception as err:
205  			fail_fence_agent(options, "Failed: get_nodes_list: {}".format(str(err)))
206  	
207  		return result
208  	
209  	
210  	def get_power_status(conn, options):
211  		logging.debug("get_power_status")
212  		# if this is bare metal we need to just send back the opposite of the
213  		# requested action: if on send off, if off send on
214  		if "--baremetalsolution" in options:
215  			if options.get("--action") == "on":
216  				return "off"
217  			else:
218  				return "on"
219  		# If zone is not listed for an entry we attempt to get it automatically
220  		instance = options["--plug"]
221  		zone = get_zone(conn, options, instance) if "--plugzonemap" not in options else options["--plugzonemap"][instance]
222  		instance_status = get_instance_power_status(conn, options, instance, zone)
223  		# If any of the instances do not match the intended status we return the
224  		# the opposite status so that the fence agent can change it.
225  		if instance_status != options.get("--action"):
226  			return instance_status
227  	
228  		return options.get("--action")
229  	
230  	
231  	def get_instance_power_status(conn, options, instance, zone):
232  		try:
233  			instance = retry_api_execute(
234  					options,
235  					conn.instances().get(project=options["--project"], zone=zone, instance=instance))
236  			return translate_status(instance["status"])
237  		except Exception as err:
238  			fail_fence_agent(options, "Failed: get_instance_power_status: {}".format(str(err)))
239  	
240  	
241  	def check_for_existing_operation(conn, options, instance, zone, operation_type):
242  		logging.debug("check_for_existing_operation")
243  		if "--baremetalsolution" in options:
244  			# There is no API for checking in progress operations
245  			return False
246  	
247  		project = options["--project"]
248  		target_link = universe_api_uri(options, INSTANCE_LINK.format(project, zone, instance))
249  		query_filter = '(targetLink = "{}") AND (operationType = "{}") AND (status = "RUNNING")'.format(target_link, operation_type)
250  		result = retry_api_execute(
251  				options,
252  				conn.zoneOperations().list(project=project, zone=zone, filter=query_filter, maxResults=1))
253  	
254  		if "items" in result and result["items"]:
255  			logging.info("Existing %s operation found", operation_type)
256  			return result["items"][0]
257  	
258  	
259  	def wait_for_operation(conn, options, zone, operation):
260  		if 'name' not in operation:
261  			logging.warning('Cannot wait for operation to complete, the'
262  			' requested operation will continue asynchronously')
263  			return False
264  	
265  		wait_time = 0
266  		project = options["--project"]
267  		while True:
268  			result = retry_api_execute(options, conn.zoneOperations().get(
269  				project=project,
270  				zone=zone,
271  				operation=operation['name']))
272  			if result['status'] == 'DONE':
273  				if 'error' in result:
CID (unavailable; MK=34d82493c75a3d798dc1d77350c4c77f) (#1 of 1): Copy-paste error (COPY_PASTE_ERROR):
(2) Event copy_paste_error: "options" in "raise_fence_agent(options, result["error"])" looks like a copy-paste error.
(3) Event remediation: Should it say "result" instead?
Also see events: [original]
274  					raise_fence_agent(options, result['error'])
275  				return True
276  	
277  			if "--errortimeout" in options and wait_time > int(options["--errortimeout"]):
278  				raise_fence_agent(options, "Operation did not complete before the timeout.")
279  	
280  			if "--warntimeout" in options and wait_time > int(options["--warntimeout"]):
281  				logging.warning("Operation did not complete before the timeout.")
282  				if "--runonwarn" in options:
(1) Event original: "run_command(options, options["--runonwarn"])" looks like the original copy.
Also see events: [copy_paste_error][remediation]
283  					run_command(options, options["--runonwarn"])
284  				return False
285  	
286  			wait_time = wait_time + 1
287  			time.sleep(1)
288  	
289  	
290  	def set_power_status(conn, options):
291  		logging.debug("set_power_status")
292  		instance = options["--plug"]
293  		# If zone is not listed for an entry we attempt to get it automatically
294  		zone = get_zone(conn, options, instance) if "--plugzonemap" not in options else options["--plugzonemap"][instance]
295  		set_instance_power_status(conn, options, instance, zone, options["--action"])
296  	
297  	
298  	def set_instance_power_status(conn, options, instance, zone, action):
299  		logging.info("Setting power status of %s in zone %s", instance, zone)
300  		project = options["--project"]
301  	
302  		try:
303  			if action == "off":
304  				logging.info("Issuing poweroff of %s in zone %s", instance, zone)
305  				operation = check_for_existing_operation(conn, options, instance, zone, "stop")
306  				if operation and "--earlyexit" in options:
307  					return
308  				if not operation:
309  					operation = retry_api_execute(
310  							options,
311  							conn.instances().stop(project=project, zone=zone, instance=instance))
312  				logging.info("Poweroff command completed, waiting for the operation to complete")
313  				if wait_for_operation(conn, options, zone, operation):
314  					logging.info("Poweroff of %s in zone %s complete", instance, zone)
315  			elif action == "on":
316  				logging.info("Issuing poweron of %s in zone %s", instance, zone)
317  				operation = check_for_existing_operation(conn, options, instance, zone, "start")
318  				if operation and "--earlyexit" in options:
319  					return
320  				if not operation:
321  					operation = retry_api_execute(
322  							options,
323  							conn.instances().start(project=project, zone=zone, instance=instance))
324  				if wait_for_operation(conn, options, zone, operation):
325  					logging.info("Poweron of %s in zone %s complete", instance, zone)
326  		except Exception as err:
327  			fail_fence_agent(options, "Failed: set_instance_power_status: {}".format(str(err)))
328  	
329  	def power_cycle(conn, options):
330  		logging.debug("power_cycle")
331  		instance = options["--plug"]
332  		# If zone is not listed for an entry we attempt to get it automatically
333  		zone = get_zone(conn, options, instance) if "--plugzonemap" not in options else options["--plugzonemap"][instance]
334  		return power_cycle_instance(conn, options, instance, zone)
335  	
336  	
337  	def power_cycle_instance(conn, options, instance, zone):
338  		logging.info("Issuing reset of %s in zone %s", instance, zone)
339  		project = options["--project"]
340  	
341  		try:
342  			operation = check_for_existing_operation(conn, options, instance, zone, "reset")
343  			if operation and "--earlyexit" in options:
344  				return True
345  			if not operation:
346  				operation = retry_api_execute(
347  						options,
348  						conn.instances().reset(project=project, zone=zone, instance=instance))
349  			logging.info("Reset command sent, waiting for the operation to complete")
350  			if wait_for_operation(conn, options, zone, operation):
351  				logging.info("Reset of %s in zone %s complete", instance, zone)
352  			return True
353  		except Exception as err:
354  			logging.exception("Failed: power_cycle")
355  			raise err
356  	
357  	
358  	def get_zone(conn, options, instance):
359  		logging.debug("get_zone");
360  		project = options['--project']
361  		fl = 'name="%s"' % instance
362  		request = replace_api_uri(options, conn.instances().aggregatedList(project=project, filter=fl))
363  		while request is not None:
364  			response = request.execute()
365  			zones = response.get('items', {})
366  			for zone in zones.values():
367  				for inst in zone.get('instances', []):
368  					if inst['name'] == instance:
369  						return inst['zone'].split("/")[-1]
370  			request = replace_api_uri(options, conn.instances().aggregatedList_next(
371  					previous_request=request, previous_response=response))
372  		raise_fence_agent(options, "Unable to find instance %s" % (instance))
373  	
374  	
375  	def get_metadata(metadata_key, params=None, timeout=None):
376  		"""Performs a GET request with the metadata headers.
377  	
378  		Args:
379  			metadata_key: string, the metadata to perform a GET request on.
380  			params: dictionary, the query parameters in the GET request.
381  			timeout: int, timeout in seconds for metadata requests.
382  	
383  		Returns:
384  			HTTP response from the GET request.
385  	
386  		Raises:
387  			urlerror.HTTPError: raises when the GET request fails.
388  		"""
389  		logging.debug("get_metadata");
390  		timeout = timeout or 60
391  		metadata_url = os.path.join(METADATA_SERVER, metadata_key)
392  		params = urlparse.urlencode(params or {})
393  		url = '%s?%s' % (metadata_url, params)
394  		request = urlrequest.Request(url, headers=METADATA_HEADERS)
395  		request_opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))
396  		return request_opener.open(request, timeout=timeout * 1.1).read().decode("utf-8")
397  	
398  	
399  	def define_new_opts():
400  		all_opt["zone"] = {
401  			"getopt" : ":",
402  			"longopt" : "zone",
403  			"help" : "--zone=[name]                  Zone, e.g. us-central1-b",
404  			"shortdesc" : "Zone.",
405  			"required" : "0",
406  			"order" : 2
407  		}
408  		all_opt["project"] = {
409  			"getopt" : ":",
410  			"longopt" : "project",
411  			"help" : "--project=[name]               Project ID",
412  			"shortdesc" : "Project ID.",
413  			"required" : "0",
414  			"order" : 3
415  		}
416  		all_opt["stackdriver-logging"] = {
417  			"getopt" : "",
418  			"longopt" : "stackdriver-logging",
419  			"help" : "--stackdriver-logging          Enable Logging to Stackdriver",
420  			"shortdesc" : "Stackdriver-logging support.",
421  			"longdesc" : "If enabled IP failover logs will be posted to stackdriver logging.",
422  			"required" : "0",
423  			"order" : 4
424  		}
425  		all_opt["baremetalsolution"] = {
426  			"getopt" : "",
427  			"longopt" : "baremetalsolution",
428  			"help" : "--baremetalsolution            Enable on bare metal",
429  			"shortdesc" : "If enabled this is a bare metal offering from google.",
430  			"required" : "0",
431  			"order" : 5
432  		}
433  		all_opt["apitimeout"] = {
434  			"getopt" : ":",
435  			"type" : "second",
436  			"longopt" : "apitimeout",
437  			"help" : "--apitimeout=[seconds]         Timeout to use for API calls",
438  			"shortdesc" : "Timeout in seconds to use for API calls, default is 60.",
439  			"required" : "0",
440  			"default" : 60,
441  			"order" : 6
442  		}
443  		all_opt["retries"] = {
444  			"getopt" : ":",
445  			"type" : "integer",
446  			"longopt" : "retries",
447  			"help" : "--retries=[retries]            Number of retries on failure for API calls",
448  			"shortdesc" : "Number of retries on failure for API calls, default is 3.",
449  			"required" : "0",
450  			"default" : 3,
451  			"order" : 7
452  		}
453  		all_opt["retrysleep"] = {
454  			"getopt" : ":",
455  			"type" : "second",
456  			"longopt" : "retrysleep",
457  			"help" : "--retrysleep=[seconds]         Time to sleep between API retries",
458  			"shortdesc" : "Time to sleep in seconds between API retries, default is 5.",
459  			"required" : "0",
460  			"default" : 5,
461  			"order" : 8
462  		}
463  		all_opt["serviceaccount"] = {
464  			"getopt" : ":",
465  			"longopt" : "serviceaccount",
466  			"help" : "--serviceaccount=[filename]    Service account json file location e.g. serviceaccount=/somedir/service_account.json",
467  			"shortdesc" : "Service Account to use for authentication to the google cloud APIs.",
468  			"required" : "0",
469  			"order" : 9
470  		}
471  		all_opt["plugzonemap"] = {
472  			"getopt" : ":",
473  			"longopt" : "plugzonemap",
474  			"help" : "--plugzonemap=[plugzonemap]    Comma separated zone map when fencing multiple plugs",
475  			"shortdesc" : "Comma separated zone map when fencing multiple plugs.",
476  			"required" : "0",
477  			"order" : 10
478  		}
479  		all_opt["proxyhost"] = {
480  			"getopt" : ":",
481  			"longopt" : "proxyhost",
482  			"help" : "--proxyhost=[proxy_host]       The proxy host to use, if one is needed to access the internet (Example: 10.122.0.33)",
483  			"shortdesc" : "If a proxy is used for internet access, the proxy host should be specified.",
484  			"required" : "0",
485  			"order" : 11
486  		}
487  		all_opt["proxyport"] = {
488  			"getopt" : ":",
489  			"type" : "integer",
490  			"longopt" : "proxyport",
491  			"help" : "--proxyport=[proxy_port]       The proxy port to use, if one is needed to access the internet (Example: 3127)",
492  			"shortdesc" : "If a proxy is used for internet access, the proxy port should be specified.",
493  			"required" : "0",
494  			"order" : 12
495  		}
496  		all_opt["earlyexit"] = {
497  			"getopt" : "",
498  			"longopt" : "earlyexit",
499  			"help" : "--earlyexit                    Return early if reset is already in progress",
500  			"shortdesc" : "If an existing reset operation is detected, the fence agent will return before the operation completes with a 0 return code.",
501  			"required" : "0",
502  			"order" : 13
503  		}
504  		all_opt["warntimeout"] = {
505  			"getopt" : ":",
506  			"type" : "second",
507  			"longopt" : "warntimeout",
508  			"help" : "--warntimeout=[warn_timeout]   Timeout seconds before logging a warning and returning a 0 status code",
509  			"shortdesc" : "If the operation is not completed within the timeout, the cluster operations are allowed to continue.",
510  			"required" : "0",
511  			"order" : 14
512  		}
513  		all_opt["errortimeout"] = {
514  			"getopt" : ":",
515  			"type" : "second",
516  			"longopt" : "errortimeout",
517  			"help" : "--errortimeout=[error_timeout] Timeout seconds before failing and returning a non-zero status code",
518  			"shortdesc" : "If the operation is not completed within the timeout, cluster is notified of the operation failure.",
519  			"required" : "0",
520  			"order" : 15
521  		}
522  		all_opt["runonwarn"] = {
523  			"getopt" : ":",
524  			"longopt" : "runonwarn",
525  			"help" : "--runonwarn=[run_on_warn]      If a timeout occurs and warning is generated, run the supplied command",
526  			"shortdesc" : "If a timeout would occur while running the agent, then the supplied command is run.",
527  			"required" : "0",
528  			"order" : 16
529  		}
530  		all_opt["runonfail"] = {
531  			"getopt" : ":",
532  			"longopt" : "runonfail",
533  			"help" : "--runonfail=[run_on_fail]      If a failure occurs, run the supplied command",
534  			"shortdesc" : "If a failure would occur while running the agent, then the supplied command is run.",
535  			"required" : "0",
536  			"order" : 17
537  		}
538  	
539  	
540  	def main():
541  		conn = None
542  	
543  		device_opt = ["port", "no_password", "zone", "project", "stackdriver-logging",
544  			"method", "baremetalsolution", "apitimeout", "retries", "retrysleep",
545  			"serviceaccount", "plugzonemap", "proxyhost", "proxyport", "earlyexit",
546  			"warntimeout", "errortimeout", "runonwarn", "runonfail"]
547  	
548  		atexit.register(atexit_handler)
549  	
550  		define_new_opts()
551  	
552  		all_opt["power_timeout"]["default"] = "60"
553  		all_opt["method"]["default"] = "cycle"
554  		all_opt["method"]["help"] = "-m, --method=[method]          Method to fence (onoff|cycle) (Default: cycle)"
555  	
556  		options = check_input(device_opt, process_input(device_opt))
557  	
558  		docs = {}
559  		docs["shortdesc"] = "Fence agent for GCE (Google Cloud Engine)"
560  		docs["longdesc"] = "fence_gce is a Power Fencing agent for GCE (Google Cloud " \
561  				   "Engine). It uses the googleapiclient library to connect to GCE.\n" \
562  				   "googleapiclient can be configured with Google SDK CLI or by " \
563  				   "executing 'gcloud auth application-default login'.\n" \
564  				   "For instructions see: https://cloud.google.com/compute/docs/tutorials/python-guide"
565  		docs["vendorurl"] = "http://cloud.google.com"
566  		show_docs(options, docs)
567  	
568  		run_delay(options)
569  	
570  		# Detect the universe domain before creating any Google Cloud clients, and
571  		# export it for client libraries that support it and for child processes.
572  		options["universe_domain"] = get_universe_domain(options)
573  		if options["universe_domain"] != DEFAULT_UNIVERSE_DOMAIN:
574  			logging.debug("Using universe domain %s" % options["universe_domain"])
575  			os.environ[UNIVERSE_DOMAIN_ENV] = options["universe_domain"]
576  		else:
577  			os.environ.pop(UNIVERSE_DOMAIN_ENV, None)
578  	
579  		# Prepare logging
580  		if options.get('--verbose') is None:
581  			logging.getLogger('googleapiclient').setLevel(logging.ERROR)
582  			logging.getLogger('oauth2client').setLevel(logging.ERROR)
583  		if options.get('--stackdriver-logging') is not None and options.get('--plug'):
584  			try:
585  				import google.cloud.logging.handlers
586  				for logger_name in ('google.cloud', 'google.auth', 'google_auth_httplib2'):
587  					logging.getLogger(logger_name).propagate = False
588  				client_kwargs = {}
589  				if options["universe_domain"] != DEFAULT_UNIVERSE_DOMAIN:
590  					client_kwargs["client_options"] = {
591  						"api_endpoint": "https://logging.%s" % options["universe_domain"]
592  					}
593  				try:
594  					client = google.cloud.logging.Client(**client_kwargs)
595  				except TypeError:
596  					client = google.cloud.logging.Client()
597  				handler = google.cloud.logging.handlers.CloudLoggingHandler(client, name=options['--plug'])
598  				handler.setLevel(logging.INFO)
599  				formatter = logging.Formatter('gcp:stonith "%(message)s"')
600  				handler.setFormatter(formatter)
601  				root_logger = logging.getLogger()
602  				if options.get('--verbose') is None:
603  					root_logger.setLevel(logging.INFO)
604  				root_logger.addHandler(handler)
605  			except Exception as err:
606  				logging.error('Couldn\'t initialize google.cloud.logging (%s), '
607  					'disabling Stackdriver-logging support', str(err))
608  	
609  	  # if apitimeout is defined we set the socket timeout, if not we keep the
610  	  # socket default which is 60s
611  		if options.get("--apitimeout"):
612  			socket.setdefaulttimeout(options["--apitimeout"])
613  	
614  		# Prepare cli
615  		try:
616  			serviceaccount = options.get("--serviceaccount")
617  			if serviceaccount:
618  				scope = ['https://www.googleapis.com/auth/cloud-platform']
619  				logging.debug("using credentials from service account")
620  				try:
621  					from google.oauth2.service_account import Credentials as ServiceAccountCredentials
622  					credentials = ServiceAccountCredentials.from_service_account_file(filename=serviceaccount, scopes=scope)
623  				except ImportError:
624  					from oauth2client.service_account import ServiceAccountCredentials
625  					credentials = ServiceAccountCredentials.from_json_keyfile_name(serviceaccount, scope)
626  			else:
627  				try:
628  					from googleapiclient import _auth
629  					credentials = _auth.default_credentials();
630  				except:
631  					credentials = GoogleCredentials.get_application_default()
632  				logging.debug("using application default credentials")
633  	
634  			build_kwargs = {'cache_discovery': False}
635  			if options.get("--proxyhost") and options.get("--proxyport"):
636  				try:
637  					import socks
638  				except ImportError:
639  					from httplib2 import socks
640  				proxy_info = httplib2.ProxyInfo(
641  					proxy_type=socks.PROXY_TYPE_HTTP,
642  					proxy_host=options.get("--proxyhost"),
643  					proxy_port=int(options.get("--proxyport")))
644  				build_kwargs['http'] = credentials.authorize(httplib2.Http(proxy_info=proxy_info))
645  			else:
646  				build_kwargs['credentials'] = credentials
647  	
648  			if options["universe_domain"] != DEFAULT_UNIVERSE_DOMAIN:
649  				build_kwargs['discoveryServiceUrl'] = (
650  					'https://{api}.%s/$discovery/rest?version={apiVersion}'
651  					% options["universe_domain"]
652  				)
653  				try:
654  					conn = googleapiclient.discovery.build(
655  						'compute', 'v1', static_discovery=True, **build_kwargs)
656  				except TypeError:
657  					conn = googleapiclient.discovery.build(
658  						'compute', 'v1', **build_kwargs)
659  			else:
660  				conn = googleapiclient.discovery.build('compute', 'v1', **build_kwargs)
661  		except SSLError as err:
662  			fail_fence_agent(options, "Failed: Create GCE compute v1 connection: {}\n\nThis might be caused by old versions of httplib2.".format(str(err)))
663  		except Exception as err:
664  			fail_fence_agent(options, "Failed: Create GCE compute v1 connection: {}".format(str(err)))
665  	
666  		# Get project and zone
667  		if not options.get("--project"):
668  			try:
669  				options["--project"] = get_metadata('project/project-id')
670  			except Exception as err:
671  				fail_fence_agent(options, "Failed retrieving GCE project. Please provide --project option: {}".format(str(err)))
672  	
673  		try:
674  			image = get_metadata('instance/image')
675  			options["image"] = image[image.rindex('/')+1:]
676  		except Exception as err:
677  			options["image"] = "unknown"
678  	
679  		if "--baremetalsolution" in options:
680  			options["--zone"] = "none"
681  	
682  		# Populates zone automatically if missing from the command
683  		zones = [] if not "--zone" in options else options["--zone"].split(",")
684  		options["--plugzonemap"] = {}
685  		if "--plug" in options:
686  			for i, instance in enumerate(options["--plug"].split(",")):
687  				if len(zones) == 1:
688  					# If only one zone is specified, use it across all plugs
689  					options["--plugzonemap"][instance] = zones[0]
690  					continue
691  	
692  				if len(zones) - 1 >= i:
693  					# If we have enough zones specified with the --zone flag use the zone at
694  					# the same index as the plug
695  					options["--plugzonemap"][instance] = zones[i]
696  					continue
697  	
698  				try:
699  					# In this case we do not have a zone specified so we attempt to detect it
700  					options["--plugzonemap"][instance] = get_zone(conn, options, instance)
701  				except Exception as err:
702  					fail_fence_agent(options, "Failed retrieving GCE zone. Please provide --zone option: {}".format(str(err)))
703  	
704  		# Operate the fencing device
705  		result = fence_action(conn, options, set_power_status, get_power_status, get_nodes_list, power_cycle)
706  		sys.exit(result)
707  	
708  	if __name__ == "__main__":
709  		main()
710