functions/billing/main.py (117 lines of code) (raw):

# Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # [START functions_billing_limit] # [START functions_billing_limit_appengine] # [START functions_billing_stop] # [START functions_billing_slack] import base64 import json import os # [END functions_billing_stop] # [END functions_billing_limit] # [END functions_billing_limit_appengine] # [END functions_billing_slack] # [START functions_billing_limit] # [START functions_billing_limit_appengine] # [START functions_billing_stop] from googleapiclient import discovery # [END functions_billing_stop] # [END functions_billing_limit] # [END functions_billing_limit_appengine] # [START functions_billing_slack] import slack from slack.errors import SlackApiError # [END functions_billing_slack] # [START functions_billing_limit] # [START functions_billing_stop] PROJECT_ID = os.getenv("GCP_PROJECT") PROJECT_NAME = f"projects/{PROJECT_ID}" # [END functions_billing_stop] # [END functions_billing_limit] # [START functions_billing_slack] # See https://api.slack.com/docs/token-types#bot for more info BOT_ACCESS_TOKEN = "xxxx-111111111111-abcdefghidklmnopq" CHANNEL = "C0XXXXXX" slack_client = slack.WebClient(token=BOT_ACCESS_TOKEN) def notify_slack(data, context): pubsub_message = data # For more information, see # https://cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification_format try: notification_attr = json.dumps(pubsub_message["attributes"]) except KeyError: notification_attr = "No attributes passed in" try: notification_data = base64.b64decode(data["data"]).decode("utf-8") except KeyError: notification_data = "No data passed in" # This is just a quick dump of the budget data (or an empty string) # You can modify and format the message to meet your needs budget_notification_text = f"{notification_attr}, {notification_data}" try: slack_client.api_call( "chat.postMessage", json={"channel": CHANNEL, "text": budget_notification_text}, ) except SlackApiError: print("Error posting to Slack") # [END functions_billing_slack] # [START functions_billing_stop] def stop_billing(data, context): pubsub_data = base64.b64decode(data["data"]).decode("utf-8") pubsub_json = json.loads(pubsub_data) cost_amount = pubsub_json["costAmount"] budget_amount = pubsub_json["budgetAmount"] if cost_amount <= budget_amount: print(f"No action necessary. (Current cost: {cost_amount})") return if PROJECT_ID is None: print("No project specified with environment variable") return billing = discovery.build( "cloudbilling", "v1", cache_discovery=False, ) projects = billing.projects() billing_enabled = __is_billing_enabled(PROJECT_NAME, projects) if billing_enabled: __disable_billing_for_project(PROJECT_NAME, projects) else: print("Billing already disabled") def __is_billing_enabled(project_name, projects): """ Determine whether billing is enabled for a project @param {string} project_name Name of project to check if billing is enabled @return {bool} Whether project has billing enabled or not """ try: res = projects.getBillingInfo(name=project_name).execute() return res["billingEnabled"] except KeyError: # If billingEnabled isn't part of the return, billing is not enabled return False except Exception: print( "Unable to determine if billing is enabled on specified project, assuming billing is enabled" ) return True def __disable_billing_for_project(project_name, projects): """ Disable billing for a project by removing its billing account @param {string} project_name Name of project disable billing on """ body = {"billingAccountName": ""} # Disable billing try: res = projects.updateBillingInfo(name=project_name, body=body).execute() print(f"Billing disabled: {json.dumps(res)}") except Exception: print("Failed to disable billing, possibly check permissions") # [END functions_billing_stop] # [START functions_billing_limit] ZONE = "us-west1-b" def limit_use(data, context): pubsub_data = base64.b64decode(data["data"]).decode("utf-8") pubsub_json = json.loads(pubsub_data) cost_amount = pubsub_json["costAmount"] budget_amount = pubsub_json["budgetAmount"] if cost_amount <= budget_amount: print(f"No action necessary. (Current cost: {cost_amount})") return compute = discovery.build( "compute", "v1", cache_discovery=False, ) instances = compute.instances() instance_names = __list_running_instances(PROJECT_ID, ZONE, instances) __stop_instances(PROJECT_ID, ZONE, instance_names, instances) def __list_running_instances(project_id, zone, instances): """ @param {string} project_id ID of project that contains instances to stop @param {string} zone Zone that contains instances to stop @return {Promise} Array of names of running instances """ res = instances.list(project=project_id, zone=zone).execute() if "items" not in res: return [] items = res["items"] running_names = [i["name"] for i in items if i["status"] == "RUNNING"] return running_names def __stop_instances(project_id, zone, instance_names, instances): """ @param {string} project_id ID of project that contains instances to stop @param {string} zone Zone that contains instances to stop @param {Array} instance_names Names of instance to stop @return {Promise} Response from stopping instances """ if not len(instance_names): print("No running instances were found.") return for name in instance_names: instances.stop(project=project_id, zone=zone, instance=name).execute() print(f"Instance stopped successfully: {name}") # [END functions_billing_limit] # [START functions_billing_limit_appengine] APP_NAME = os.getenv("GCP_PROJECT") def limit_use_appengine(data, context): pubsub_data = base64.b64decode(data["data"]).decode("utf-8") pubsub_json = json.loads(pubsub_data) cost_amount = pubsub_json["costAmount"] budget_amount = pubsub_json["budgetAmount"] if cost_amount <= budget_amount: print(f"No action necessary. (Current cost: {cost_amount})") return appengine = discovery.build("appengine", "v1", cache_discovery=False) apps = appengine.apps() # Get the target app's serving status target_app = apps.get(appsId=APP_NAME).execute() current_status = target_app["servingStatus"] # Disable target app, if necessary if current_status == "SERVING": print(f"Attempting to disable app {APP_NAME}...") body = {"servingStatus": "USER_DISABLED"} apps.patch(appsId=APP_NAME, updateMask="serving_status", body=body).execute() # [END functions_billing_limit_appengine]