From 5069b6243078861d052a34ac6d40be071856fce7 Mon Sep 17 00:00:00 2001 From: Damian Lance Date: Tue, 7 Sep 2021 09:52:39 -0700 Subject: [PATCH 1/6] Update README.md --- tools/google-cloud-support-slackbot/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/google-cloud-support-slackbot/README.md b/tools/google-cloud-support-slackbot/README.md index 68dc3039bf..f82523c27e 100644 --- a/tools/google-cloud-support-slackbot/README.md +++ b/tools/google-cloud-support-slackbot/README.md @@ -14,6 +14,8 @@ The app currently supports the following commands: * /google-cloud-support case-details [case_number] -- pull all of the case data as json * /google-cloud-support sitrep -- report of all active cases in the org +**If you encounter any issues with this application's operations or setup, please file your issue here on GitHub or ping a member of your account team for assistance. This application is not supported by the Google Cloud Support team.** + # Setup Guide **Before proceeding, you will need Premium Support to use the Cloud Support API and by association the slackbot** From 7fd0b8736b40af3528437a1ff3ba45b2b2b11abb Mon Sep 17 00:00:00 2001 From: TheLanceLord Date: Wed, 23 Feb 2022 12:18:00 -0800 Subject: [PATCH 2/6] Refactor and rearchitecture of the Google Cloud Support Slackbot code. Makes the code container friendly by integrating it with Firestore --- .../.dockerignore | 7 + .../google-cloud-support-slackbot/Dockerfile | 50 +++ tools/google-cloud-support-slackbot/README.md | 247 ++++++------ .../SupportCase.py | 110 ++++++ .../{ => archive/standalone}/default | 2 +- .../google_cloud_support_slackbot.py | 2 +- .../archive/standalone/requirements.txt | 55 +++ .../case_details.py | 64 ++++ .../case_not_found.py | 57 +++ .../case_updates.py | 164 ++++++++ tools/google-cloud-support-slackbot/env.list | 10 + .../firestore_delete_cases.py | 54 +++ .../firestore_write.py | 96 +++++ .../get_firestore_cases.py | 73 ++++ .../get_firestore_first_in.py | 84 +++++ .../get_firestore_tracked_cases.py | 55 +++ .../get_parent.py | 57 +++ .../list_tracked_cases.py | 61 +++ .../list_tracked_cases_all.py | 61 +++ tools/google-cloud-support-slackbot/main.py | 241 ++++++++++++ .../notify_slack.py | 78 ++++ .../post_help_message.py | 72 ++++ .../requirements.txt | 51 +-- tools/google-cloud-support-slackbot/sitrep.py | 95 +++++ .../slackbot_integration_test.py | 354 ++++++++++++++++++ .../stop_tracking.py | 83 ++++ .../support_add_comment.py | 97 +++++ .../support_change_priority.py | 92 +++++ .../support_close_case.py | 85 +++++ .../support_create_case.py | 133 +++++++ .../support_escalate.py | 114 ++++++ .../support_subscribe_email.py | 111 ++++++ .../track_case.py | 86 +++++ 33 files changed, 2834 insertions(+), 167 deletions(-) create mode 100755 tools/google-cloud-support-slackbot/.dockerignore create mode 100755 tools/google-cloud-support-slackbot/Dockerfile create mode 100755 tools/google-cloud-support-slackbot/SupportCase.py rename tools/google-cloud-support-slackbot/{ => archive/standalone}/default (96%) rename tools/google-cloud-support-slackbot/{ => archive/standalone}/google_cloud_support_slackbot.py (99%) create mode 100644 tools/google-cloud-support-slackbot/archive/standalone/requirements.txt create mode 100755 tools/google-cloud-support-slackbot/case_details.py create mode 100755 tools/google-cloud-support-slackbot/case_not_found.py create mode 100755 tools/google-cloud-support-slackbot/case_updates.py create mode 100644 tools/google-cloud-support-slackbot/env.list create mode 100755 tools/google-cloud-support-slackbot/firestore_delete_cases.py create mode 100755 tools/google-cloud-support-slackbot/firestore_write.py create mode 100755 tools/google-cloud-support-slackbot/get_firestore_cases.py create mode 100755 tools/google-cloud-support-slackbot/get_firestore_first_in.py create mode 100755 tools/google-cloud-support-slackbot/get_firestore_tracked_cases.py create mode 100755 tools/google-cloud-support-slackbot/get_parent.py create mode 100755 tools/google-cloud-support-slackbot/list_tracked_cases.py create mode 100755 tools/google-cloud-support-slackbot/list_tracked_cases_all.py create mode 100755 tools/google-cloud-support-slackbot/main.py create mode 100755 tools/google-cloud-support-slackbot/notify_slack.py create mode 100755 tools/google-cloud-support-slackbot/post_help_message.py create mode 100755 tools/google-cloud-support-slackbot/sitrep.py create mode 100755 tools/google-cloud-support-slackbot/slackbot_integration_test.py create mode 100755 tools/google-cloud-support-slackbot/stop_tracking.py create mode 100755 tools/google-cloud-support-slackbot/support_add_comment.py create mode 100755 tools/google-cloud-support-slackbot/support_change_priority.py create mode 100755 tools/google-cloud-support-slackbot/support_close_case.py create mode 100755 tools/google-cloud-support-slackbot/support_create_case.py create mode 100755 tools/google-cloud-support-slackbot/support_escalate.py create mode 100755 tools/google-cloud-support-slackbot/support_subscribe_email.py create mode 100755 tools/google-cloud-support-slackbot/track_case.py diff --git a/tools/google-cloud-support-slackbot/.dockerignore b/tools/google-cloud-support-slackbot/.dockerignore new file mode 100755 index 0000000000..5511566f81 --- /dev/null +++ b/tools/google-cloud-support-slackbot/.dockerignore @@ -0,0 +1,7 @@ +Dockerfile +error.log +env.list +freeze.txt +.dockerignore +archive/* +archive diff --git a/tools/google-cloud-support-slackbot/Dockerfile b/tools/google-cloud-support-slackbot/Dockerfile new file mode 100755 index 0000000000..29ba3b2113 --- /dev/null +++ b/tools/google-cloud-support-slackbot/Dockerfile @@ -0,0 +1,50 @@ +# Copyright 2022 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. + +FROM debian:10 + +LABEL author="Damian Lance" +LABEL created="2022-01-19" +LABEL last_updated="2022-01-19" + +# Essential environment variables +ENV ORG_ID="YOUR NUMERIC ORG ID" +ENV SLACK_TOKEN="YOUR SLACK TOKEN" +ENV SIGNING_SECRET="YOUR SLACK SIGNING SECRET" +ENV API_KEY="YOUR API KEY FOR GOOGLE CLOUD SUPPORT AND CLOUD FIRESTORE" + +# Testing environment variables +ENV TEST_CHANNEL_ID="SLACK CHANNEL ID FOR TESTING" +ENV TEST_CHANNEL_NAME="SLACK CHANNEL NAME FOR TESTING" +ENV TEST_USER_ID="SLACK USER_ID FOR TESTING" +ENV TEST_USER_NAME="SLACK USER_NAME FOR TESTING" +ENV PROJECT_ID="GOOGLE CLOUD PROJECT ID FOR SUPPORT CASES" +ENV PROJECT_NUMBER="GOOGLE CLOUD PROJECT NUMBER FOR SUPPORT CASES" + +WORKDIR /google-cloud-support-slackbot + +COPY . ./ + +RUN apt-get update; \ + apt-get -y install \ + python3-pip \ + curl \ + libffi-dev \ + libssl-dev; \ + pip3 install -r requirements.txt; + +EXPOSE 80 + +ENTRYPOINT /google-cloud-support-slackbot/main.py + diff --git a/tools/google-cloud-support-slackbot/README.md b/tools/google-cloud-support-slackbot/README.md index f82523c27e..18e5e0c90e 100644 --- a/tools/google-cloud-support-slackbot/README.md +++ b/tools/google-cloud-support-slackbot/README.md @@ -8,6 +8,9 @@ The app currently supports the following commands: * /google-cloud-support track-case [case_number] -- case updates will be posted to this channel * /google-cloud-support add-comment [case_number] [comment] -- adds a comment to the case * /google-cloud-support change-priority [case_number] [priority, e.g. P2] -- changes the priority of the case +* /google-cloud-support subscribe [case number] [email 1] ... [email n] -- subscribes the given emails addresses to the case to receive updates to their inboxes. This overwrites the previous list of emails +* /google-cloud-support escalate [case number] [reason] [justification] -- escalates the support case. Reason must be either RESOLUTION_TIME, TECHNICAL_EXPERTISE, or BUSINESS_IMPACT +* /google-cloud-support close-case [case number] -- closes a case * /google-cloud-support stop-tracking [case_number] -- case updates will no longer be posted to this channel * /google-cloud-support list-tracked-cases -- lists all cases being tracked in this channel * /google-cloud-support list-tracked-cases-all -- lists all cases being tracked in the workspace @@ -21,126 +24,132 @@ The app currently supports the following commands: **Before proceeding, you will need Premium Support to use the Cloud Support API and by association the slackbot** Setting up your first Slack app can be a daunting task, which is why we are providing a step-by-step guide. -## Setup Part 1 - Allow list the Support API +## Setup Part 1 - Slack App Phase 1 -To get access to the API, you will need to send your Techincal Account Manager the following: - -1. The **org id** where you have Premium Support enabled -2. A **project id** where the API will be allow listed -3. The name of a **service account** in the project from step 2, with the service account having the following roles at the org level: - 1. **Tech Support Editor** - 1. **Org Viewer** -4. The **email addresses** of the people that will be enabling the API in the project - -Your Techincal Account Manager will file a request with the Support API team to give you access. The team typically processes these requests within 24 hours - -## Setup Part 2 - Google Cloud Phase 1 - -In the first phase of our Google Cloud setup, we will verify that our network is setup properly, create a lightweight VM to house our bot, and enable our Cloud Support API and create ourselves an API key. Go to [Google Cloud](https://cloud.google.com/console). **These steps need to be carried out in the project you specified in Part 1 of this setup guide.** - -### Networking - -From **VPC network > Firewall rules**, verify rules exist to **allow SSH and HTTP**. - -1. **If your project doesn't have a VPC, you will need to create one from VPC networks**. Select **Automatic** for your Subnet creation mode, and **allow-ssh** from **Firewall rules** -2. If it doesn't exist, create the following firewall rule: - 1. Name: `default-allow-http` - 1. Priority: `1000` - 1. Direction: `Ingress` - 1. Action on match: `Allow` - 1. Targets: `Specified target tags` - 1. Target tags: `http-server` - 1. Source filter: `IP ranges` - 1. Source IP ranges: `0.0.0.0/0` - 1. Protocols and Ports: `Specified protocols and ports` - 1. tcp: `80` -3. If an SSH firewall rule doesn't exist, create the following firewall rule: - 1. Name: `default-allow-ssh` - 1. Priority: `65534` - 1. Direction: `Ingress` - 1. Action on match: `Allow` - 1. Targets: `All instances in the network` - 1. Source filter: `IP ranges` - 1. Source IP ranges: `0.0.0.0/0` - 1. Protocols and Ports: `Specified protocols and ports` - 1. tcp: `22` - -*Note that if you had to create the SSH firewall rule in Step 3, you will want to disable it after you complete the entire setup* - -### VM - -Go to **Compute Engine > VM instances** and perform the following: - -1. Click **+ Create Instance** - 1. Under **Machine Configuration**, set the **Machine type** field to **e2-micro**. This should suffice for most implementations. If your team makes heavy use of the Cloud Support and the bot, you may need to upgrade the machine type - 1. Under **Identity and API access > Service Account**, select your **service account** that was allow listed for the Cloud Support API - 1. Under **Firewall**, select **Allow HTTP traffic**. If this option isn't available and you create the firewall rule in the Networking steps, then you will want to contact your Networking team about policies that may be preventing HTTP traffic - 1. Click to expand **Management, security, disks, networking, sole tenancy** - 1. Select the **Networking** tab - 1. Under **Network interfaces**, click the network interface box - 1. Set **Network** to the VPC where you have your firewall rules - 1. Under **External IP**, select **Create IP address**. Choose whichever name and network service tier you prefer - 1. Click **Create** - -### API Enablement and the API Key - -From **APIs & Services > Library** ... - -1. Search for and enable the **Cloud Logging API** -2. Search for and enable the **Cloud Support API** - -From **APIs & Services > Credentials** +Go to [Slack Apps](http://api.slack.com/apps) to do the following: -1. Click **+Create** and select **API key** -2. Copy your key and choose to **Restrict Key** - 1. Under **Application restrictions**, you may select **IP addresses** to restrict usage the VM you created - 1. Under **API restrictions**, select **Restrict Key** and from the **Select APIs** dropdown, click **Google Cloud Support API** +1. Click **Create New App** and select **From an app manifest** +2. Select the workspace where you want to add the app and then click **Next** +3. Copy and paste in the following YAML and then click **Next**: +''' +display_information: + name: Google Cloud Support Bot +features: + bot_user: + display_name: Google Cloud Support Bot + always_online: false + slash_commands: + - command: /google-cloud-support + url: https://CLOUDRUN_SERVICE_URL/google-cloud-support + description: Track and manage your Google Cloud support cases in Slack. Use /google-cloud-support help for the list of commands + usage_hint: "[command] [parameter 1] [parameter 2]" + should_escape: false +oauth_config: + scopes: + bot: + - chat:write + - channels:history + - commands +settings: + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false +''' +4. Click **Create** +5. Under **Settings > Basic Information**, scroll down to **Display Information** and upload the [google_cloud_support_buddy_big.png](google_cloud_sup$ +6. Go to **Settings > Basic Information** and under **Building Apps for Slack > Install your app**, click **Install to Workspace**. On the next screen click **Allow**. You may need Slack admin approval to install the app +7. Go to **Settings > Basic Information** and under **App Credentials** copy the `Signing Secret`. You will need this for **Setup Part 2** +8. Go to the **Features > OAuth & Permissions** page, under **OAuth Tokens for Your Workspace**. Copy the `Bot User OAuth Token`. You will need this for **Setup Part 2** + +## Setup Part 2 - Google Cloud + +Go to [Google Cloud](https://console.cloud.google.com/) to do the following: + +1. Go to the project dropdown at the top of the page and select it. From the list, select the project where you want to host the app, or create a new project for it. After completing the rest of the steps, the app will have support ticket access for all projects in your org +2. Click the **Activate Cloud Shell** button to open the Cloud Shell Terminal. Confirm the Cloud Shell is set to the project where you want to host the app. If it isn't, set it using the `gcloud config set project PROJECT_ID` command. Authorize the command if prompted. +3. **WARNING**: Running step 4 will delete the default VPC and its associated firewall rules as they aren't needed by our app when it operates in Cloud Run. If you dont want to do this, delete lines 8-12 in the step 4's code block +4. Update the following code block with your `SIGNING_SECRET` and `SLACK_TOKEN` from **Setup Part 1**, and then run it in your **Cloud Shell**: +''' +SIGNING_SECRET=SIGNING_SECRET +SLACK_TOKEN=SLACK_TOKEN +TAG=2.0 +alias gcurl='curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json"'; +ORG_ID=$(gcurl -X POST https://cloudresourcemanager.googleapis.com/v1/projects/$DEVSHELL_PROJECT_ID:getAncestry | jq '.ancestor[] | select(.resourceId.type == "organization")' | jq '.resourceId.id' | sed 's/"//g'); +PROJECT_NUMBER=`gcloud projects list --filter="${DEVSHELL_PROJECT_ID}" --format="value(PROJECT_NUMBER)"`; +gcloud services enable firestore.googleapis.com cloudsupport.googleapis.com logging.googleapis.com compute.googleapis.com iam.googleapis.com artifactregistry.googleapis.com run.googleapis.com serviceusage.googleapis.com appengine.googleapis.com; +yes | gcloud compute firewall-rules delete default-allow-icmp; +yes | gcloud compute firewall-rules delete default-allow-internal; +yes | gcloud compute firewall-rules delete default-allow-rdp; +yes | gcloud compute firewall-rules delete default-allow-ssh; +yes | gcloud compute networks delete default; +gcloud iam service-accounts create support-slackbot \ + --description="Used by the Google Cloud Support Slackbot" \ + --display-name="Support Slackbot"; +gcloud organizations add-iam-policy-binding $ORG_ID \ + --member="serviceAccount:support-slackbot@$DEVSHELL_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/cloudsupport.techSupportEditor"; +gcloud organizations add-iam-policy-binding $ORG_ID \ + --member="serviceAccount:support-slackbot@$DEVSHELL_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/datastore.owner"; +gcloud organizations add-iam-policy-binding $ORG_ID \ + --member="serviceAccount:support-slackbot@$DEVSHELL_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/resourcemanager.organizationViewer"; +gcloud auth configure-docker us-central1-docker.pkg.dev +gcloud artifacts repositories create google-cloud-support-slackbot \ + --repository-format=Docker \ + --location=us-central1 \ + --description="Docker images for the Google Cloud Support Slackbot"; +gcloud app create --region=us-central; +gcloud alpha firestore databases create --region=us-central; +docker pull thelancelord/google-cloud-support-slackbot:2.0; +docker tag thelancelord/google-cloud-support-slackbot:2.0 us-central1-docker.pkg.dev/$DEVSHELL_PROJECT_ID/google-cloud-support-slackbot/google-cloud-support-slackbot:2.0; +docker push us-central1-docker.pkg.dev/$DEVSHELL_PROJECT_ID/google-cloud-support-slackbot/google-cloud-support-slackbot:2.0; +gcurl https://apikeys.googleapis.com/v2/projects/$PROJECT_NUMBER/locations/global/keys \ + --request POST \ + --data '{ + "displayName": "Support Slackbot", + "restrictions": { + "api_targets": [ + { + "service": "cloudsupport.googleapis.com", + "methods": [ + "Get*" + ] + }, + { + "service" : "firestore.googleapis.com", + "methods": [ + "Get*" + ] + } + ] + }, + }'; +KEY_PATH=$(gcurl https://apikeys.googleapis.com/v2/projects/$PROJECT_NUMBER/locations/global/keys | jq ".keys[].name" | sed 's/"//g' | sed -n '([^\/]+$)'); +API_KEY="${KEY_PATH##*/}"; +gcloud run deploy google-cloud-support-slackbot \ +--image=us-central1-docker.pkg.dev/$DEVSHELL_PROJECT_ID/google-cloud-support-slackbot/google-cloud-support-slackbot:$TAG \ +--allow-unauthenticated \ +--service-account=support-slackbot@$DEVSHELL_PROJECT_ID.iam.gserviceaccount.com \ +--min-instances=1 \ +--max-instances=3 \ +--set-env-vars=TEST_CHANNEL_ID=$TEST_CHANNEL_ID,TEST_CHANNEL_NAME=$TEST_CHANNEL_NAME,TEST_USER_ID=$TEST_USER_ID,TEST_USER_NAME=$TEST_USER_NAME,ORG_ID=$ORG_ID,SLACK_TOKEN=$SLACK_TOKEN,SIGNING_SECRET=$SIGNING_SECRET,API_KEY=$API_KEY,PROJECT_ID=$DEVSHELL_PROJECT_ID,TEST_PROJECT_NUMBER=$PROJECT_NUMBER \ +--no-use-http2 \ +--no-cpu-throttling \ +--platform=managed \ +--region=us-central1 \ +--port=5000 \ +--project=$DEVSHELL_PROJECT_ID; +''' +This will output a URL. Copy this URL to use in **Setup Part 3**. If you need to find this URL again, you can find it under **Cloud Run** by clicking on the google-cloud-support-slackbot service. You will find the URL near the top of the Service details page ## Setup Part 3 - Slack App -Go to [Slack Apps](http://api.slack.com/apps) to do the following: - -1. Click **Create New App** and select **From scratch**. Name your app `Google Cloud Support Bot` and select your workspace -2. Under **Settings > Basic Information**, scroll down to **Display Information** and upload the [google_cloud_support_buddy_big.png](google_cloud_support_buddy_big.png) or an icon of your choosing -3. Go to **Features > Slash Commands** and create the following command: - 1. Command: `/google-cloud-support ` - 1. Request URL: `http:///google-cloud-support` - 1. Short description: `Track and manage your Google Cloud support cases in Slack. Use /google-cloud-support help for the list of commands` - 1. Usage Hint: `[command] [parameter 1] [parameter 2] [parameter 3]` -4. Go to **Features > OAuth & Permissions**. Scroll down to **Scopes** and add the **chat:write** scope. Add the **commands** scope if it isn't listed already listed -5. At the top of the **Features > OAuth & Permissions** page, under **OAuth Tokens for Your Workspace**, click **Install to Workspace**. Copy the token. You may need Slack admin approval to install the app -6. Go to **Settings > Basic Information** and under **App Credentials** copy the `Signing Secret` - -## Setup Part 4 - Google Cloud Phase 2 - -Return to [Google Cloud](https://cloud.google.com/console) and from **Compute Engine > VM instances**, perform the following: - -1. SSH into the VM that you created in part 2 of this setup guide -2. Run the following commands: - 1. `sudo apt-get update` - 1. `sudo apt-get -y install subversion` - 1. `sudo apt-get -y install python3-pip` - 1. `sudo apt-get -y install nginx` - 1. `cd /` - 1. `sudo svn export https://github.com/GoogleCloudPlatform/professional-services/trunk/tools/google-cloud-support-slackbot` - 1. `cd /google-cloud-support-slackbot` - 1. Use sudo to open the `default` file with your editor of choice, and replace with the external ip address of your VM. Then save and close the file - 1. `sudo mv default /etc/nginx/sites-available/` - 1. Use sudo to open the `.env` file with your editor of choice. Enter your API Key, Slack Token, and numeric org id in their respective locations. Then save and close the file - 1. `sudo chmod +x google_cloud_support_slackbot.py` -3. Close the SSH session -4. From Compute Engine > VM instances, click your VM name to go to your VM instance details -5. Stop the VM -6. Once the VM is stopped, click the 'EDIT' button -7. Scroll down to the Custom metadata section and add the following key-value pair: - 1. key: `startup-script` - 1. value: - `cd /google-cloud-support-slackbot` - `pip3 install -r requirements.txt` - `/google-cloud-support-slackbot/google_cloud_support_slackbot.py` -8. Scroll to the bottom of the page and click 'Save' -9. Start your VM +Return to [Slack Apps](http://api.slack.com/apps) to do the following: +1. Go to **Features > Slash Commands** and click the **pencil icon**: + 1. Update the `Request URL`'s `CLOUDRUN_SERVICE_URL` placeholder with the url generated in Setup Part 2 and then click **Save** + ## Testing To verify that everything was setup correctly, do the following: @@ -154,12 +163,4 @@ To verify that everything was setup correctly, do the following: With that you should be all setup! And as a reminder, if you had to create the SSH firewall rule, it is recommended that you go back and disable it. If you ever need to SSH into the machine you can always enable the rule again as needed. -As the Cloud Support API continues to expand and we collect more feedback for requested features, we will release newer versions of the bot and move the previous version into the archive folder. To replace your current bot with the latest version you will only need to do the following: - -1. SSH into your VM instance -2. Run the following commands: - 1. `cd /google-cloud-support-slackbot` - 1. `sudo svn export --force https://github.com/GoogleCloudPlatform/professional-services/trunk/tools/google-cloud-support-slackbot/google_cloud_support_slackbot.py` - 1. `sudo chmod +x google_cloud_support_slackbot.py` -3. Close your SSH session -4. Stop and Start your VM +As the Cloud Support API continues to expand and we collect more feedback for requested features, we will release newer versions of the bot and move the previous version into the archive folder. diff --git a/tools/google-cloud-support-slackbot/SupportCase.py b/tools/google-cloud-support-slackbot/SupportCase.py new file mode 100755 index 0000000000..3e7702f58d --- /dev/null +++ b/tools/google-cloud-support-slackbot/SupportCase.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# Copyright 2021 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. + +import os +import re +import logging +import time +import requests +from datetime import datetime +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +class SupportCase: + """ + Represent a Google Cloud Support Case. + + Attributes + ---------- + case_number : str + a unique string of numbers that is the id for the case + resource_name : str + a unique string including the org or project id and the case id, examples: + organizations/12345/cases/67890 + projects/12345/cases/67890 + case_title : str + the title the user gave the case when they created it + description : str + the user's description of the case as provided in the support ticket + escalated : bool + whether or not a case has been escalated. This field doesn't exist in + the response until after a case has been escalated. True means the case + is escalated + case_creator : str + name of the user that opened the support case + create_time : str + timestamp of when the case was created + update_time : str + timestamp of the last update made to the case + priority : str + the current priority of the case, represented as S0, S1, S2, S3, or S4 + state : str + the status of the support ticket. Can be NEW, IN_PROGRESS_GOOGLE_SUPPORT, + ACTION_REQUIRED, SOLUTION_PROVIDED, or CLOSED + comment_list : list + all public comments made on the case as strings. Comments are sorted + with newest comments at the top + """ + + def __init__(self, caseobj): + """ + Parameters + ---------- + caseobj : json + json for an individual case + """ + MAX_RETRIES = 3 + API_KEY = os.environ.get('API_KEY') + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery' + '/rest?key={}&labels=V2_TRUSTED_TESTER&version=v2beta' + .format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + self.case_number = re.search('(?:cases/)([0-9]+)', caseobj['name'])[1] + self.resource_name = caseobj['name'] + self.case_title = caseobj['displayName'] + self.description = caseobj['description'] + if 'escalated' in caseobj: + self.escalated = caseobj['escalated'] + else: + self.escalated = False + self.case_creator = caseobj['creator']['displayName'] + self.create_time = str(datetime.fromisoformat( + caseobj['createTime'].replace('Z', '+00:00'))) + self.update_time = str(datetime.fromisoformat( + caseobj['updateTime'].replace('Z', '+00:00'))) + self.priority = caseobj['severity'].replace('S', 'P') + self.state = caseobj['state'] + self.comment_list = [] + case_comments = support_service.cases().comments() + request = case_comments.list(parent=self.resource_name) + while request is not None: + try: + comments = request.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + time.sleep(1) + else: + if "comments" in comments: + for comment in comments['comments']: + self.comment_list.append(comment) + request = case_comments.list_next(request, comments) diff --git a/tools/google-cloud-support-slackbot/default b/tools/google-cloud-support-slackbot/archive/standalone/default similarity index 96% rename from tools/google-cloud-support-slackbot/default rename to tools/google-cloud-support-slackbot/archive/standalone/default index b263cec014..3b151f02eb 100644 --- a/tools/google-cloud-support-slackbot/default +++ b/tools/google-cloud-support-slackbot/archive/standalone/default @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tools/google-cloud-support-slackbot/google_cloud_support_slackbot.py b/tools/google-cloud-support-slackbot/archive/standalone/google_cloud_support_slackbot.py similarity index 99% rename from tools/google-cloud-support-slackbot/google_cloud_support_slackbot.py rename to tools/google-cloud-support-slackbot/archive/standalone/google_cloud_support_slackbot.py index d84a3dcec5..60056a9a85 100644 --- a/tools/google-cloud-support-slackbot/google_cloud_support_slackbot.py +++ b/tools/google-cloud-support-slackbot/archive/standalone/google_cloud_support_slackbot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tools/google-cloud-support-slackbot/archive/standalone/requirements.txt b/tools/google-cloud-support-slackbot/archive/standalone/requirements.txt new file mode 100644 index 0000000000..5244ef1ad4 --- /dev/null +++ b/tools/google-cloud-support-slackbot/archive/standalone/requirements.txt @@ -0,0 +1,55 @@ +aiohttp==3.7.4.post0 +asn1crypto==0.24.0 +async-timeout==3.0.1 +attrs==21.2.0 +cachetools==4.2.2 +certifi==2021.5.30 +click==7.1.2 +crcmod==1.7 +cryptography==2.6.1 +distro-info==0.21 +entrypoints==0.3 +Flask==1.1.4 +gevent==21.8.0 +google-api-core==1.31.0 +google-api-python-client==2.12.0 +google-auth==1.32.1 +google-auth-httplib2==0.1.0 +googleapis-common-protos==1.53.0 +greenlet==1.1.1 +httplib2==0.19.1 +idna==2.10 +importlib-metadata==4.5.0 +itsdangerous==1.1.0 +Jinja2==2.11.3 +keyring==17.1.1 +keyrings.alt==3.1.1 +MarkupSafe==1.1.1 +multidict==5.1.0 +packaging==21.0 +protobuf==3.17.3 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycrypto==2.6.1 +pyee==7.0.4 +PyGObject==3.30.4 +pyparsing==2.4.7 +python-apt==1.8.4.3 +python-dotenv==0.18.0 +pytz==2021.1 +pyxdg==0.25 +requests==2.25.1 +rsa==4.7.2 +SecretStorage==2.3.1 +six==1.16.0 +slackclient==2.9.3 +slackeventsapi==2.2.1 +typing-extensions==3.10.0.0 +unattended-upgrades==0.1 +uritemplate==3.0.1 +urllib3==1.26.6 +Werkzeug==1.0.1 +yarl==1.6.3 +zipp==3.4.1 +zope.event==4.5.0 +zope.interface==5.4.0 diff --git a/tools/google-cloud-support-slackbot/case_details.py b/tools/google-cloud-support-slackbot/case_details.py new file mode 100755 index 0000000000..0487cc4da7 --- /dev/null +++ b/tools/google-cloud-support-slackbot/case_details.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import json +import logging +from case_not_found import case_not_found +from get_firestore_cases import get_firestore_cases + +logger = logging.getLogger(__name__) + + +def case_details(channel_id, case, user_id): + """ + Sends the data of a single case as json to the channel where the request originated. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + cases = get_firestore_cases() + break_flag = False + + for fs_case in cases: + if case == fs_case['case_number']: + pretty_json = json.dumps(fs_case, indent=4, sort_keys=True) + client.chat_postMessage( + channel=channel_id, + text=f"Here are the details on case {case}: \n{pretty_json}") + break_flag = True + break + + if break_flag is False: + case_not_found(channel_id, user_id, case) + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = 'xxxxxxxx' + user_id = os.environ.get('TEST_USER_ID') + case_details(channel_id, case, user_id) + case = os.environ.get('TEST_CASE') + case_details(channel_id, case, user_id) diff --git a/tools/google-cloud-support-slackbot/case_not_found.py b/tools/google-cloud-support-slackbot/case_not_found.py new file mode 100755 index 0000000000..b22089eaaa --- /dev/null +++ b/tools/google-cloud-support-slackbot/case_not_found.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging +from datetime import datetime + +logger = logging.getLogger(__name__) + + +def case_not_found(channel_id, user_id, case): + """ + Informs the user of their case could not be found. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + case : str + unique id of the case + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + try: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"Case {case} could not be found in your org. If this case was recently" + " created, please give the system 60 seconds to fetch it. Otherwise," + " double check your case number or confirm the org being tracked" + " with your Slack admin.") + except slack.errors.SlackApiError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + user_id = os.environ.get('TEST_USER_ID') + case = "xxxxxxxx" + case_not_found(channel_id, user_id, case) diff --git a/tools/google-cloud-support-slackbot/case_updates.py b/tools/google-cloud-support-slackbot/case_updates.py new file mode 100755 index 0000000000..4d4ec72749 --- /dev/null +++ b/tools/google-cloud-support-slackbot/case_updates.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import time +import requests +from datetime import datetime +from googleapiclient.discovery import build_from_document +from firestore_write import firestore_write +from get_firestore_cases import get_firestore_cases +from get_firestore_first_in import get_firestore_first_in +from firestore_delete_cases import firestore_delete_cases +from notify_slack import notify_slack +from SupportCase import SupportCase + +logger = logging.getLogger(__name__) + + +def case_updates(is_test): + """ + Infinite loop that pulls all of the open Google Cloud support cases for our org and + their associated public comments every 15 seconds and compares it to the cases and + comments from the previous pull. If any change is detected between the two versions + of the case, the change is posted to any channel that is tracking it. + + Parameters + ---------- + is_test : bool + flag indicating if we are running the loop a single time for testing + """ + ORG_ID = os.environ.get('ORG_ID') + API_KEY = os.environ.get('API_KEY') + MAX_RETRIES = 3 + query_string = 'organization="organizations/{}" AND state=OPEN'.format(ORG_ID) + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/' + 'rest?key={}&labels=V2_TRUSTED_TESTER&version=v2beta' + .format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + while True: + loop_skip = False + sleep_timer = 10 + closed_cases = [] + cases = get_firestore_cases() + req = support_service.cases().search(query=query_string) + try: + resp = req.execute(num_retries=MAX_RETRIES).get('cases', []) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + time.sleep(5) + continue + + temp_cases = [] + + for case in resp: + try: + temp_case = SupportCase(case) + except NameError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + loop_skip = True + break + else: + temp_cases.append(vars(temp_case)) + + if loop_skip: + time.sleep(5) + continue + + # Check for cases that have closed since the last loop and notify slack + for fs_case in cases: + delete_entry = True + if fs_case['update_time'] == '2100-12-31 23:59:59+00:00': + delete_entry = False + else: + for t_case in temp_cases: + if t_case['case_number'] == fs_case['case_number']: + delete_entry = False + break + if delete_entry: + fs_case['update_time'] = '2100-12-31 23:59:59+00:00' + guid = firestore_write('cases', fs_case) + first_doc_in = get_firestore_first_in( + fs_case['case_number'], + fs_case['update_time']) + if first_doc_in: + if guid == first_doc_in['guid']: + notify_slack(fs_case['case_number'], 'closed', '') + closed_cases.append(fs_case['case_number']) + + # Check for existing cases that have a new update time. Post their relevant update + # to the channels that are tracking those cases. + for t_case in temp_cases: + is_new = True + for fs_case in cases: + if t_case['case_number'] == fs_case['case_number']: + is_new = False + if not t_case['update_time'] == fs_case['update_time']: + guid = firestore_write('cases', t_case) + first_doc_in = get_firestore_first_in( + t_case['case_number'], + t_case['update_time']) + if fs_case['comment_list'] != t_case['comment_list']: + if 'googleSupport' in t_case['comment_list'][0]['creator']: + if guid == first_doc_in['guid']: + notify_slack( + t_case['case_number'], + 'comment', + t_case['comment_list'][0]['body']) + if fs_case['priority'] != t_case['priority']: + if guid == first_doc_in['guid']: + notify_slack( + t_case['case_number'], + 'priority', + t_case['priority']) + if fs_case['escalated'] != t_case['escalated']: + if t_case['escalated']: + if guid == first_doc_in['guid']: + notify_slack( + t_case, + 'escalated', + t_case['escalated']) + else: + if guid == first_doc_in['guid']: + notify_slack( + t_case['case_number'], + 'de-escalated', + t_case['escalated']) + + if is_new: + firestore_write('cases', t_case) + + # Wait to try again so we don't spam the API + time.sleep(sleep_timer) + + # Delete closed cases after waiting to minimize duplicate Slack updates + for case in closed_cases: + firestore_delete_cases(case) + + if is_test: + break + + +if __name__ == "__main__": + is_test = True + case_updates(is_test) diff --git a/tools/google-cloud-support-slackbot/env.list b/tools/google-cloud-support-slackbot/env.list new file mode 100644 index 0000000000..fda4de6509 --- /dev/null +++ b/tools/google-cloud-support-slackbot/env.list @@ -0,0 +1,10 @@ +ORG_ID=YOUR_ORG_ID +SLACK_TOKEN=YOUR_SLACK_TOKEN +SIGNING_SECRET=YOUR_SIGNING_SECRET +API_KEY=YOUR_API_KEY +PROJECT_ID=YOUR_PROJECT_ID +TEST_CHANNEL_ID=SLACK_CHANNEL_ID +TEST_CHANNEL_NAME=SLACK_CHANNEL_NAME +TEST_USER_ID=SLACK_USER_ID +TEST_USER_NAME=SLACK_USER_NAME +TEST_PROJECT_NUMBER=PROJECT_NUMBER_FOR_TESTING diff --git a/tools/google-cloud-support-slackbot/firestore_delete_cases.py b/tools/google-cloud-support-slackbot/firestore_delete_cases.py new file mode 100755 index 0000000000..6a5b83fb9f --- /dev/null +++ b/tools/google-cloud-support-slackbot/firestore_delete_cases.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def firestore_delete_cases(case): + """ + Delete all cases from Firestore with a matching case number. + + Parameters + ---------- + case : str + unique id of the case + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + collection = 'cases' + db = firestore.client() + firestore_cases = db.collection(collection).where('case_number', '==', case).get() + + for firestore_case in firestore_cases: + fs_case = firestore_case.to_dict() + db.collection(collection).document(fs_case['guid']).delete() + + +if __name__ == "__main__": + case = os.environ.get('TEST_CASE') + firestore_delete_cases(case) diff --git a/tools/google-cloud-support-slackbot/firestore_write.py b/tools/google-cloud-support-slackbot/firestore_write.py new file mode 100755 index 0000000000..f4b45e12ba --- /dev/null +++ b/tools/google-cloud-support-slackbot/firestore_write.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import firebase_admin +import logging +import uuid +import time +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def firestore_write(collection, content) -> str: + """ + Takes the provided json and attaches a guid and timestamp to it and then writes + it to the specified collection. + + Parameters + ---------- + collection : str + name of the collection that we are writing to + content : dict + json data that we are writing + + Returns + ------- + guid + unique string that is used by the firestore_read module to determine if this + instance was the first to write the data into Firestore + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + db = firestore.client() + guid = str(uuid.uuid4()) + timestamp = time.time() + content['guid'] = guid + content['firestore_timestamp'] = timestamp + + doc_ref = db.collection(collection).document(guid) + doc_ref.set(content) + + return guid + + +if __name__ == "__main__": + project_number = os.environ.get('TEST_PROJECT_NUMBER') + case = os.environ.get('TEST_CASE') + resource_name = 'projects/{}/cases/{}'.format(project_number, case) + content = { + "case_number": case, + "resource_name": resource_name, + "case_title": "--PSO SLACKBOT TEST--", + "description": ("---Testing the firestore write functionality!---\n" + "I'm doing some work on a Slack bot that will use our Cloud" + " Support APIs. I'll be testing out the API functionality and" + " need open cases to do so. Please ignore this case.\n\nThanks"), + "escalated": False, + "case_creator": "Slackbot Admin", + "create_time": "2021-07-12 17:55:11+00:00", + "update_time": "2021-07-12 22:34:21+00:00", + "priority": "P4", + "state": "IN_PROGRESS_GOOGLE_SUPPORT", + "comment_list": [{ + "name": (resource_name + "/comments/xxxxxxxxxxxxxxxxxx"), + "createTime": "2021-07-12T21:34:19Z", + "creator": { + "displayName": "Slackbot Admin", + "googleSupport": True + }, + "body": "This is a public case comment", + "plainTextBody": "This is a public case comment" + }] + } + collection = 'cases' + print(str(firestore_write(collection, content))) diff --git a/tools/google-cloud-support-slackbot/get_firestore_cases.py b/tools/google-cloud-support-slackbot/get_firestore_cases.py new file mode 100755 index 0000000000..a6363976df --- /dev/null +++ b/tools/google-cloud-support-slackbot/get_firestore_cases.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def get_firestore_cases() -> list: + """ + Fetches the cases in Firestore and returns the copy for each case id with the + greatest timestamp value. + + Returns + ------- + support_cases + list of dicts containing the case information for all of our cases + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + db = firestore.client() + collection = 'cases' + firestore_cases = db.collection(collection).get() + support_cases = [] + + for firestore_case in firestore_cases: + fs_case = firestore_case.to_dict() + fs_timestamp = float(fs_case['firestore_timestamp']) + if len(support_cases) == 0: + support_cases.append(fs_case) + else: + i = 0 + case_number = fs_case['case_number'] + temp_cases = support_cases + case_exists = False + for support_case in temp_cases: + if (support_case['case_number'] == case_number): + case_exists = True + if fs_timestamp > float(support_case['firestore_timestamp']): + support_cases[i] = fs_case + break + i += 1 + if case_exists is False: + support_cases.append(fs_case) + + return support_cases + + +if __name__ == "__main__": + print(str(get_firestore_cases())) diff --git a/tools/google-cloud-support-slackbot/get_firestore_first_in.py b/tools/google-cloud-support-slackbot/get_firestore_first_in.py new file mode 100755 index 0000000000..b86c910bc2 --- /dev/null +++ b/tools/google-cloud-support-slackbot/get_firestore_first_in.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore +from datetime import datetime + +logger = logging.getLogger(__name__) + + +def get_firestore_first_in(case, update_time) -> dict: + """ + Pulls all the docs for a case with the specified update time and returns the + the doc with the earliest app-generated timestamp. + + Parameters + ---------- + case : str + a unique string of numbers that is the id for the case + update_time : str + the reported time that the case was last updated + + Returns + ------- + first_doc_in + the matching document in the collection with the earliest timestamp + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + db = firestore.client() + + collection_ref = db.collection('cases') + + query = (collection_ref.where('update_time', '==', update_time) + .where('case_number', '==', case)) + docs = query.get() + first_doc_in = {} + + i = 0 + try: + first_doc_in = docs[0].to_dict() + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + return first_doc_in + firestore_timestamp = '' + for doc in docs: + doc_timestamp = doc.to_dict()['firestore_timestamp'] + if i == 0: + firestore_timestamp = doc_timestamp + elif float(doc_timestamp) < float(firestore_timestamp): + firestore_timestamp = doc_timestamp + first_doc_in = docs[i].to_dict() + i += 1 + + return first_doc_in + + +if __name__ == "__main__": + case = os.environ.get('TEST_CASE') + update_time = '2021-07-12 22:34:21+00:00' + print(str(get_firestore_first_in(case, update_time))) diff --git a/tools/google-cloud-support-slackbot/get_firestore_tracked_cases.py b/tools/google-cloud-support-slackbot/get_firestore_tracked_cases.py new file mode 100755 index 0000000000..1c82b5314e --- /dev/null +++ b/tools/google-cloud-support-slackbot/get_firestore_tracked_cases.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def get_firestore_tracked_cases() -> list: + """ + Fetches the cases in Firestore and returns the copy for each case id with the + greatest timestamp value. + + Returns + ------- + tracked_cases + list of dicts containing the tracked case information + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + db = firestore.client() + tracked_cases = [] + collection = 'tracked_cases' + firestore_tracked_cases = db.collection(collection).get() + for case in firestore_tracked_cases: + tracked_cases.append(case.to_dict()) + + return tracked_cases + + +if __name__ == "__main__": + print(str(get_firestore_tracked_cases())) diff --git a/tools/google-cloud-support-slackbot/get_parent.py b/tools/google-cloud-support-slackbot/get_parent.py new file mode 100755 index 0000000000..d2cfecdd38 --- /dev/null +++ b/tools/google-cloud-support-slackbot/get_parent.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def get_parent(case) -> str: + """ + Retrieves the full parent path for a given case id. + + Parameters + ---------- + case : str + unique id of the case + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + collection = 'cases' + db = firestore.client() + firestore_cases = db.collection(collection).where('case_number', '==', case).get() + + if firestore_cases: + return firestore_cases[0].to_dict()['resource_name'] + else: + return 'Case not found' + + +if __name__ == "__main__": + case = 'xxxxxxxx' + print(get_parent(case)) + case = os.environ.get('TEST_CASE') + print(get_parent(case)) diff --git a/tools/google-cloud-support-slackbot/list_tracked_cases.py b/tools/google-cloud-support-slackbot/list_tracked_cases.py new file mode 100755 index 0000000000..abbcc45264 --- /dev/null +++ b/tools/google-cloud-support-slackbot/list_tracked_cases.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +from get_firestore_tracked_cases import get_firestore_tracked_cases + + +def list_tracked_cases(channel_id, channel_name, user_id): + """ + Display all of the tracked Google Cloud support cases for the current channel + to the user that submitted the command. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + channel_name : str + user designated channel name. For users to understand where their cases are being + tracked in Slack + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + tracked_cases = get_firestore_tracked_cases() + + local_tracked_cases = [] + for tc in tracked_cases: + if tc['channel_id'] == channel_id: + local_tracked_cases.append(tc['case']) + if len(local_tracked_cases) > 0: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"Currently tracking cases {local_tracked_cases}") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"There are no cases currently being tracked in this channel") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + channel_name = os.environ.get('TEST_CHANNEL_NAME') + user_id = os.environ.get('TEST_USER_ID') + list_tracked_cases(channel_id, channel_name, user_id) diff --git a/tools/google-cloud-support-slackbot/list_tracked_cases_all.py b/tools/google-cloud-support-slackbot/list_tracked_cases_all.py new file mode 100755 index 0000000000..2a5ff2e2e3 --- /dev/null +++ b/tools/google-cloud-support-slackbot/list_tracked_cases_all.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +from get_firestore_tracked_cases import get_firestore_tracked_cases + + +def list_tracked_cases_all(channel_id, user_id): + """ + Display all the Google Cloud support cases being tracked in the Slack worskpace + to the user that submitted the command. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + tracked_cases = get_firestore_tracked_cases() + + all_tracked_cases = [] + for tc in tracked_cases: + temp = { + "channel": tc['channel_name'], + "case": tc['case'] + } + all_tracked_cases.append(temp) + + if len(all_tracked_cases) > 0: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"Currently tracking cases {all_tracked_cases}") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"There are no cases currently being tracked in Slack") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + user_id = os.environ.get('TEST_USER_ID') + list_tracked_cases_all(channel_id, user_id) diff --git a/tools/google-cloud-support-slackbot/main.py b/tools/google-cloud-support-slackbot/main.py new file mode 100755 index 0000000000..4de9879585 --- /dev/null +++ b/tools/google-cloud-support-slackbot/main.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import slack +import os +import requests +import logging +import multiprocessing as mp +from flask import Flask, request, Response +from slackeventsapi import SlackEventAdapter +from googleapiclient.discovery import build_from_document +from datetime import datetime +from gevent.pywsgi import WSGIServer +from case_details import case_details +from case_updates import case_updates +from get_firestore_tracked_cases import get_firestore_tracked_cases +from list_tracked_cases import list_tracked_cases +from list_tracked_cases_all import list_tracked_cases_all +from post_help_message import post_help_message +from sitrep import sitrep +from stop_tracking import stop_tracking +from support_add_comment import support_add_comment +from support_change_priority import support_change_priority +from support_close_case import support_close_case +from support_escalate import support_escalate +from support_subscribe_email import support_subscribe_email +from track_case import track_case + +# To run this on the cheapest possible VM, we will only log Warnings and Errors +logging.basicConfig(filename='error.log') +logger = logging.getLogger('werkzeug') +logger.setLevel(logging.WARNING) + +logging.warning('Started at: {}'.format(datetime.now())) + +app = Flask(__name__) + +client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) +ORG_ID = os.environ.get('ORG_ID') +SLACK_SIGNING_SECRET = os.environ.get('SIGNING_SECRET') +API_KEY = os.environ.get('API_KEY') +MAX_RETRIES = 3 + +slack_events = SlackEventAdapter(SLACK_SIGNING_SECRET, "/slack/events", app) + +# Get our discovery doc and build our service +r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta' + .format(API_KEY)) +r.raise_for_status() +support_service = build_from_document(r.json()) + +tracked_cases = get_firestore_tracked_cases() + + +# Handle all calls to the support bot +@app.route('/google-cloud-support', methods=['POST']) +def gcp_support() -> Response: + """ + Takes a user's slash command from Slack and executes it. Multiprocessing is used + on commands that modify the case to prevent Slack timeouts. + + Parameters + ---------- + request : Request + message and metadata that was submitted by Slack + Returns + ------- + Response + tells Slack that the command was received and not to throw a timeout alert + 200 + HTTP 200 OK + 403 + HTTP 403 Forbidden, received if the request signature can't be verified + """ + # Verify that the request is coming from our Slack + slack_timestamp = request.headers.get('X-Slack-Request-Timestamp') + slack_signature = request.headers.get('X-Slack-Signature') + result = slack_events.server.verify_signature(slack_timestamp, slack_signature) + if result is False: + return Response(), 403 + + data = request.form + channel_id = data.get('channel_id') + channel_name = data.get('channel_name') + user_id = data.get('user_id') + user_name = data.get('user_name') + user_inputs = data.get('text').split(' ', 1) + command = user_inputs[0] + + if command == 'track-case': + try: + case = user_inputs[1] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The track-case command expects argument [case_number]." + " The case number provided did not match with any cases in your org") + track_case(channel_id, channel_name, case, user_id) + elif command == 'add-comment': + try: + parameters = user_inputs[1].split(' ', 1) + case = parameters[0] + comment = parameters[1] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The add-comment command expects arguments [case_number] [comment]." + " The comment does not need to be encapsulated in quotes." + " Your case number did not match with any cases in your org.") + p = mp.Process( + target=support_add_comment, + args=(channel_id, case, comment, user_id, user_name,)) + p.start() + elif command == 'change-priority': + try: + parameters = user_inputs[1].split(' ', 1) + case = parameters[0] + priority = parameters[1] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The change-priority command expects arguments " + "[case_number] [priority, must be either P1|P2|P3|P4]." + " Your case number did not match with any cases in your org," + " or the priority did not match the expected values.") + p = mp.Process( + target=support_change_priority, + args=(channel_id, case, priority, user_id,)) + p.start() + elif command == 'subscribe': + try: + parameters = user_inputs[1].split(' ', 1) + case = parameters[0] + emails = parameters[1].split() + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The subscribe command expects arguments " + "[case_number] [email_1] ... [email_n]." + " Your case number did not match with any cases in your org," + " or your command did not match the expected input format.") + p = mp.Process( + target=support_subscribe_email, + args=(channel_id, case, emails, user_id,)) + p.start() + elif command == 'escalate': + try: + parameters = user_inputs[1].split(' ', 2) + case = parameters[0] + reason = parameters[1] + justification = parameters[2] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The escalate command expects arguments " + "[reason, must be either RESOLUTION_TIME|TECHNICAL_EXPERTISE" + "|BUSINESS_IMPACT] [justification]. The justification does not need to" + " be encapsulated in quotes. Either your case number did not match with" + " any cases in your org, the reason did not match one of the expected" + " values, or the justification was missing") + p = mp.Process( + target=support_escalate, + args=(channel_id, case, user_id, reason, justification, user_name)) + p.start() + elif command == 'close-case': + try: + case = user_inputs[1] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The close-case command expects arguments [case_number]") + support_close_case(channel_id, case, user_id) + elif command == 'stop-tracking': + try: + case = user_inputs[1] + except IndexError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="The stop-tracking command expects arguments [case_number].") + stop_tracking(channel_id, channel_name, case, user_id) + elif command == 'list-tracked-cases': + list_tracked_cases(channel_id, channel_name, user_id) + elif command == 'list-tracked-cases-all': + list_tracked_cases_all(channel_id, user_id) + elif command == 'case-details': + case = user_inputs[1] + case_details(channel_id, case, user_id) + elif command == 'sitrep': + sitrep(channel_id, user_id) + elif command == 'help': + context = '' + post_help_message(channel_id, user_id, context) + else: + context == "Sorry, that wasn't a recognized command. " + post_help_message(channel_id, user_id, context) + + return Response(), 200 + + +if __name__ == "__main__": + mp.set_start_method('spawn') + p = mp.Process(target=case_updates, args=(False,)) + p.start() + http_server = WSGIServer(('', 5000), app) + http_server.serve_forever() + p.join() diff --git a/tools/google-cloud-support-slackbot/notify_slack.py b/tools/google-cloud-support-slackbot/notify_slack.py new file mode 100755 index 0000000000..edb038c188 --- /dev/null +++ b/tools/google-cloud-support-slackbot/notify_slack.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging +from get_firestore_tracked_cases import get_firestore_tracked_cases + +logger = logging.getLogger(__name__) + + +def notify_slack(case, update_type, update_text): + """ + Sends update messages to Slack. + + Parameters + ---------- + case : str + unique id of the case + update_type : str + specifies what was changed in the case + update_text : str + update relevant content that is injected into the Slack message + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + tracked_cases = get_firestore_tracked_cases() + for t in tracked_cases: + if t['case'] == case: + if update_type == 'comment': + client.chat_postMessage( + channel=t['channel_id'], + text="You have an update from your support engineer on case" + f" {case}: \n{update_text}") + elif update_type == 'priority': + client.chat_postMessage( + channel=t['channel_id'], + text=f"The priority of case {case} has been changed to {update_text}") + elif update_type == 'closed': + client.chat_postMessage( + channel=t['channel_id'], + text=f"Case {case} has been closed") + elif update_type == 'escalated': + client.chat_postMessage( + channel=t['channel_id'], + text=f"Case {case} has been escalated") + elif update_type == 'de-escalated': + client.chat_postMessage( + channel=t['channel_id'], + text=f"Case {case} has been de-escalated") + + +if __name__ == "__main__": + case = os.environ.get('TEST_CASE') + update_type = "comment" + update_text = "This is a test comment that doesn't actually appear on the case." + notify_slack(case, update_type, update_text) + update_type = "priority" + update_text = "Priority unchanged" + notify_slack(case, update_type, update_text) + update_type = "closed" + notify_slack(case, update_type, update_text) + update_type = "escalated" + notify_slack(case, update_type, update_text) + update_type = "de-escalated" + notify_slack(case, update_type, update_text) diff --git a/tools/google-cloud-support-slackbot/post_help_message.py b/tools/google-cloud-support-slackbot/post_help_message.py new file mode 100755 index 0000000000..5a72bb719e --- /dev/null +++ b/tools/google-cloud-support-slackbot/post_help_message.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging + +logger = logging.getLogger(__name__) + + +def post_help_message(channel_id, user_id, context): + """ + Informs the user of the app's available commands. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + context : str + Extra information to go with the help message. Usually a statement of a command + not existing + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"{context}Here are the available commands:" + "\n/google-cloud-support track-case [case number] -- case updates will be" + " posted to this channel" + "\n/google-cloud-support add-comment [case number] [comment] -- adds a comment" + " to the case" + "\n/google-cloud-support change-priority [case number] [priority, e.g. P1] --" + " changes the priority of the case" + "\n/google-cloud-support subscribe [case number] [email 1] ... [email n] --" + " subscribes the given emails addresses to the case to receive updates" + " to their inboxes. This overwrites the previous list of emails" + "\n/google-cloud-support escalate [case number] [reason] [justification] --" + " escalates the support case. Reason must be either RESOLUTION_TIME," + " TECHNICAL_EXPERTISE, or BUSINESS_IMPACT" + "\n/google-cloud-support close-case [case number] -- closes a case" + "\n/google-cloud-support stop-tracking [case number] -- case updates will no" + " longer be posted to this channel" + "\n/google-cloud-support list-tracked-cases -- lists all cases being tracked" + " in this channel" + "\n/google-cloud-support list-tracked-cases-all -- lists all cases being" + " tracked in the workspace" + "\n/google-cloud-support case-details [case_number] -- pull all of the case" + " data as json" + "\n/google-cloud-support sitrep -- report of all active cases in the org") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + user_id = os.environ.get('TEST_USER_ID') + context = 'This is a test of the post_help_message function. ' + post_help_message(channel_id, user_id, context) diff --git a/tools/google-cloud-support-slackbot/requirements.txt b/tools/google-cloud-support-slackbot/requirements.txt index 5244ef1ad4..47ec0eccbf 100644 --- a/tools/google-cloud-support-slackbot/requirements.txt +++ b/tools/google-cloud-support-slackbot/requirements.txt @@ -1,55 +1,22 @@ -aiohttp==3.7.4.post0 -asn1crypto==0.24.0 -async-timeout==3.0.1 -attrs==21.2.0 -cachetools==4.2.2 -certifi==2021.5.30 -click==7.1.2 -crcmod==1.7 cryptography==2.6.1 -distro-info==0.21 -entrypoints==0.3 +firebase==3.0.1 +firebase-admin==5.1.0 Flask==1.1.4 gevent==21.8.0 google-api-core==1.31.0 google-api-python-client==2.12.0 google-auth==1.32.1 google-auth-httplib2==0.1.0 +google-cloud==0.34.0 +google-cloud-core==2.2.1 +google-cloud-firestore==2.3.4 +google-cloud-storage==1.43.0 +google-crc32c==1.3.0 +google-resumable-media==2.1.0 googleapis-common-protos==1.53.0 -greenlet==1.1.1 -httplib2==0.19.1 -idna==2.10 -importlib-metadata==4.5.0 -itsdangerous==1.1.0 -Jinja2==2.11.3 -keyring==17.1.1 -keyrings.alt==3.1.1 -MarkupSafe==1.1.1 -multidict==5.1.0 -packaging==21.0 -protobuf==3.17.3 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycrypto==2.6.1 -pyee==7.0.4 -PyGObject==3.30.4 -pyparsing==2.4.7 -python-apt==1.8.4.3 -python-dotenv==0.18.0 -pytz==2021.1 -pyxdg==0.25 +grpcio==1.42.0 requests==2.25.1 -rsa==4.7.2 -SecretStorage==2.3.1 six==1.16.0 slackclient==2.9.3 slackeventsapi==2.2.1 -typing-extensions==3.10.0.0 -unattended-upgrades==0.1 -uritemplate==3.0.1 -urllib3==1.26.6 Werkzeug==1.0.1 -yarl==1.6.3 -zipp==3.4.1 -zope.event==4.5.0 -zope.interface==5.4.0 diff --git a/tools/google-cloud-support-slackbot/sitrep.py b/tools/google-cloud-support-slackbot/sitrep.py new file mode 100755 index 0000000000..f9171e6932 --- /dev/null +++ b/tools/google-cloud-support-slackbot/sitrep.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging +from get_firestore_cases import get_firestore_cases + +logger = logging.getLogger(__name__) + + +def sitrep(channel_id, user_id): + """ + Lists the following details for all cases in the org: + case id, priority, title, isEscalated, case creation time, last case update time, + case status, and case creator. + Additionally, provides a summary of the number of the number of cases open by + priority, the total number of cases, and the total number of escalated cases. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + p1 = 0 + p2 = 0 + p3 = 0 + p4 = 0 + esc_count = 0 + report = ('This is the current state of Google Cloud Support cases:' + '\n\ncase,priority,title,escalated,create_time,last_updated,state,' + 'case_creator') + cases = get_firestore_cases() + + for case in cases: + if case['priority'] == 'P1': + p1 += 1 + elif case['priority'] == 'P2': + p2 += 1 + elif case['priority'] == 'P3': + p3 += 1 + else: + p4 += 1 + + if case['escalated']: + esc_count += 1 + + report = report + '\n{},{},{},{},{},{},{},{}'.format( + case['case_number'], + case['priority'], + case['case_title'], + case['escalated'], + case['create_time'], + case['update_time'], + case['state'], + case['case_creator']) + + report = (report + '\n\n' + '\n{} P1 cases are open' + '\n{} P2 cases are open' + '\n{} P3 cases are open' + '\n{} P4 cases are open' + '\nTotal cases open: {}' + '\nEscalated cases: {}').format( + str(p1), + str(p2), + str(p3), + str(p4), + str(p1 + p2 + p3 + p4), + str(esc_count)) + + client.chat_postMessage(channel=channel_id, text=f"{report}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + user_id = os.environ.get('TEST_USER_ID') + sitrep(channel_id, user_id) diff --git a/tools/google-cloud-support-slackbot/slackbot_integration_test.py b/tools/google-cloud-support-slackbot/slackbot_integration_test.py new file mode 100755 index 0000000000..139906c0f2 --- /dev/null +++ b/tools/google-cloud-support-slackbot/slackbot_integration_test.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import unittest +from post_help_message import post_help_message +from case_not_found import case_not_found +from support_create_case import support_create_case +from firestore_write import firestore_write +from get_firestore_first_in import get_firestore_first_in +from get_firestore_cases import get_firestore_cases +from get_parent import get_parent +from case_details import case_details +from track_case import track_case +from get_firestore_tracked_cases import get_firestore_tracked_cases +from notify_slack import notify_slack +from list_tracked_cases import list_tracked_cases +from list_tracked_cases_all import list_tracked_cases_all +from sitrep import sitrep +from support_add_comment import support_add_comment +from support_change_priority import support_change_priority +from support_subscribe_email import support_subscribe_email +from support_close_case import support_close_case +from firestore_delete_cases import firestore_delete_cases +from stop_tracking import stop_tracking +from case_updates import case_updates + + +class MonolithicTestCase(unittest.TestCase): + """ + Test all of our functions and procedures except escalate. + + Attributes + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + channel_name : str + designated channel name of the channel. For users to understand where their + cases are being tracked in Slack + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + user_name : str + Slack user_name of the user that ran the command. Appended to the end of the + comment to identify who submitted submitted it, otherwise all comments will + show as coming from the case creator + project_number : str + unique number of the project where we will be creating and modifying our test case + case : str + unique id of the case + content : dict + json data that we are writing + update_time : str + the reported time that the case was last updated + guid : str + unique string that is used by the firestore_read module to determine if this + instance was the first to write the data into Firestore + resource_name : str + parent or name of the case in the format of 'projects/12345/cases/67890' or + 'organizations/12345/cases/67890' + """ + channel_id = os.environ.get('TEST_CHANNEL_ID') + channel_name = os.environ.get('TEST_CHANNEL_NAME') + user_id = os.environ.get('TEST_USER_ID') + user_name = os.environ.get('TEST_USER_NAME') + project_number = os.environ.get('TEST_PROJECT_NUMBER') + case = 'xxxxxxxx' + content = {} + update_time = '2021-07-12 22:34:21+00:00' + guid = '' + resource_name = '' + + def step01_post_help_message(self): + """ + Run the post_help_message procedure. If successful, a message will appear in Slack. + """ + context = 'This is a unit test of the post_help_message procedure. ' + post_help_message_output = post_help_message(self.channel_id, self.user_id, context) + self.assertEqual(post_help_message_output, None) + + def step02_case_not_found(self): + """ + Run the case_not_found procedure. If successful, a message will appear in Slack. + """ + case_not_found_output = case_not_found(self.channel_id, self.user_id, self.case) + self.assertEqual(case_not_found_output, None) + + def step03_support_create_case(self): + """ + Run the support_create_case function. This function is not available as a user + command as dealing with the dozens of enumerations of classification would be + a poor user experience. + """ + display_name = 'IGNORE -- Google Cloud Support Slackbot test' + description = str('This is an automatically case created by the Google Cloud' + ' Support Slackbot. Please delete this case if it is open for' + ' more than 30 minutes') + severity = 4 + classification_id = '100H41Q3DTMN0TBKCKD0SGRFDLO7AT35412MSPR9DPII4229DPPN8OBECDIG' + classification_display_name = 'Compute \u003e Compute Engine \u003e Instance' + time_zone = '-7:00' + test_case = True + support_create_case_output = support_create_case( + self.channel_id, self.user_id, self.user_name, display_name, description, + severity, classification_id, classification_display_name, time_zone, + self.project_number, test_case) + self.assertEqual(len(support_create_case_output), 8) + self.case = support_create_case_output + + def step04_firestore_write(self): + """ + Run the firestore_write function. + """ + self.resource_name = 'projects/{}/cases/{}'.format(self.project_number, self.case) + content = { + "case_number": self.case, + "resource_name": self.resource_name, + "case_title": "--PSO SLACKBOT TEST--", + "description": ("---Testing the firestore write functionality!---\n" + "I'm doing some work on a Slack bot that will use our" + " Cloud Support APIs. I'll be testing out the API" + " functionality and need open cases to do so. Please" + " ignore this case.\n\nThanks"), + "escalated": False, + "case_creator": "Slackbot Admin", + "create_time": "2021-07-12 17:55:11+00:00", + "update_time": self.update_time, + "priority": "P4", + "state": "IN_PROGRESS_GOOGLE_SUPPORT", + "comment_list": [ + { + "name": ("projects/xxxxxxxx/cases/xxxxxxxx/comments" + "/xxxxxxxxxxxxxxxxxx"), + "createTime": "2021-07-12T21:34:19Z", + "creator": { + "displayName": "Slackbot Admin", + "googleSupport": True + }, + "body": "This is a public case comment", + "plainTextBody": "This is a public case comment" + } + ] + } + collection = 'cases' + self.guid = firestore_write(collection, content) + self.assertEqual(len(self.guid), 36) + + def step05_get_firestore_first_in(self): + """ + Run the get_firestore_first_in function. + """ + first_in_case = get_firestore_first_in(self.case, self.update_time) + self.assertEqual(first_in_case['guid'], self.guid) + + def step06_get_firestore_cases(self): + """ + Run the get_firestore_cases function. + """ + cases = get_firestore_cases() + self.assertTrue(cases) + + def step07_get_parent_failure(self): + """ + Run the get_parent function and test the failure branch. + """ + parent = get_parent('xxxxxxxx') + self.assertEqual(parent, 'Case not found') + + def step08_get_parent_success(self): + """ + Run the get_parent function and test the success branch. + """ + parent = get_parent(self.case) + self.assertEqual(parent, self.resource_name) + + def step09_case_details(self): + """ + Run the case_details function. If successful, a message will appear in Slack. + """ + case_details_output = case_details(self.channel_id, self.case, self.user_id) + self.assertEqual(case_details_output, None) + + def step10_track_case(self): + """ + Run the track_case function. If successful, a message will appear in Slack. + """ + track_case_output = track_case(self.channel_id, self.channel_name, + self.case, self.user_id) + self.assertEqual(track_case_output, None) + + def step11_get_firestore_tracked_cases(self): + """ + Run the get_firestore_tracked_cases function. + """ + tracked_cases = get_firestore_tracked_cases() + self.assertTrue(tracked_cases) + + def step12_notify_slack_comment(self): + """ + Run the notify_slack procedure for comment. If successful, a message will appear in Slack. + """ + update_type = 'comment' + update_text = 'This is a test comment that doesn\'t actually appear on the case.' + notify_slack_comment_output = notify_slack(self.case, update_type, update_text) + self.assertEqual(notify_slack_comment_output, None) + + def step13_notify_slack_priority(self): + """ + Run the notify_slack procedure for priority. If successful, a message will appear in Slack. + """ + update_type = 'priority' + update_text = 'P5' + notify_slack_priority_output = notify_slack(self.case, update_type, update_text) + self.assertEqual(notify_slack_priority_output, None) + + def step14_notify_slack_closed(self): + """ + Run the notify_slack procedure for closed. If successful, a message will appear in Slack. + """ + update_type = 'closed' + update_text = '' + notify_slack_closed_output = notify_slack(self.case, update_type, update_text) + self.assertEqual(notify_slack_closed_output, None) + + def step15_notify_slack_escalated(self): + """ + Run the notify_slack procedure for escalated. If successful, a message + will appear in Slack. + """ + update_type = 'escalated' + update_text = '' + notify_slack_escalated_output = notify_slack(self.case, update_type, update_text) + self.assertEqual(notify_slack_escalated_output, None) + + def step16_notify_slack_deescalated(self): + """ + Run the notify_slack procedure for de-escalated. If successful, a message + will appear in Slack. + """ + update_type = 'de-escalated' + update_text = '' + notify_slack_deescalated_output = notify_slack(self.case, update_type, update_text) + self.assertEqual(notify_slack_deescalated_output, None) + + def step17_list_tracked_cases(self): + """ + Run the list_tracked_cases procedure. If successful, a message will appear in Slack. + """ + list_tracked_cases_output = list_tracked_cases(self.channel_id, + self.channel_name, self.user_id) + self.assertEqual(list_tracked_cases_output, None) + + def step18_list_tracked_cases_all(self): + """ + Run the list_tracked_cases_all procedure. If successful, a message will appear in Slack. + """ + list_tracked_cases_all_output = list_tracked_cases_all(self.channel_id, + self.user_id) + self.assertEqual(list_tracked_cases_all_output, None) + + def step19_sitrep(self): + """ + Run the sitrep procedure. If successful, a message will appear in Slack. + """ + sitrep_output = sitrep(self.channel_id, self.user_id) + self.assertEqual(sitrep_output, None) + + def step20_support_add_comment(self): + """ + Run the support_add_comment procedure. If successful, a message will appear in Slack. + """ + comment = 'This is a test comment generated by our testing script.' + support_add_comment_output = support_add_comment(self.channel_id, self.case, + comment, self.user_id, self.user_name) + self.assertEqual(support_add_comment_output, None) + + def step21_support_change_priority(self): + """ + Run the support_change_priority procedure. If successful, a message will appear in Slack. + """ + priority = 'P3' + support_change_priority_output = support_change_priority(self.channel_id, self.case, + priority, self.user_id) + self.assertEqual(support_change_priority_output, None) + + def step22_support_subscribe_email(self): + """ + Run the support_change_priority procedure. If successful, a message will appear in Sl$ + """ + emails = ["testaccount1@example.com", "testaccount2@example.com"] + support_subscribe_email_output = support_subscribe_email(self.channel_id, self.case, + emails, self.user_id) + self.assertEqual(support_subscribe_email_output, None) + + def step23_support_close_case(self): + """ + Run the support_close_case procedure. If successful, a message will appear in Slack. + """ + support_close_case_output = support_close_case(self.channel_id, + self.case, self.user_id) + self.assertEqual(support_close_case_output, None) + + def step24_firestore_delete_cases_output(self): + """ + Run the firestore_delete_cases procedure. + """ + firestore_delete_cases_output = firestore_delete_cases(self.case) + self.assertEqual(firestore_delete_cases_output, None) + + def step25_stop_tracking(self): + """ + Run the stop_tracking procedure. If successful, a message will appear in Slack. + """ + stop_tracking_output = stop_tracking(self.channel_id, self.channel_name, + self.case, self.user_id) + self.assertEqual(stop_tracking_output, None) + + def step26_case_updates(self): + """ + Run the case_updates procedure. + """ + case_updates_output = case_updates(True) + self.assertEqual(case_updates_output, None) + + def _steps(self): + """ + Yields the methods in this class in sorted order so we can perform our integration test + """ + for name in dir(self): + if name.startswith("step"): + yield name, getattr(self, name) + + def test_steps(self): + """ + Exceutes the methods + """ + for name, step in self._steps(): + try: + step() + except Exception as e: + self.fail("{} failed ({}: {})".format(step, type(e), e)) diff --git a/tools/google-cloud-support-slackbot/stop_tracking.py b/tools/google-cloud-support-slackbot/stop_tracking.py new file mode 100755 index 0000000000..0a2295f740 --- /dev/null +++ b/tools/google-cloud-support-slackbot/stop_tracking.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore + +logger = logging.getLogger(__name__) + + +def stop_tracking(channel_id, channel_name, case, user_id): + """ + Remove a case from the list of tracked Google Cloud support cases. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + channel_name : str + user designated channel name. For users to understand where their cases are being + tracked in Slack + case : str + unique id of the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + # Initialize the Firebase app if it hasn't already been done + if not firebase_admin._apps: + PROJECT_ID = os.environ.get('PROJECT_ID') + cred = credentials.ApplicationDefault() + firebase_admin.initialize_app(cred, { + 'projectId': PROJECT_ID, + }) + + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + collection = 'tracked_cases' + db_collection = firestore.Client().collection(collection) + tracked_cases = (db_collection + .where('case', '==', case) + .where('channel_id', '==', channel_id) + .get()) + + exists = False + for tracked_case in tracked_cases: + tc = tracked_case.to_dict() + if tc['channel_id'] == channel_id and tc['case'] == case: + db_collection.document(tc['guid']).delete() + exists = True + break + if exists: + client.chat_postMessage( + channel=channel_id, + text=f"Case {case} is no longer being tracked in {channel_name}") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, text=f"Case {case} not found in tracker for {channel_name}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + channel_name = os.environ.get('TEST_CHANNEL_NAME') + case = os.environ.get('TEST_CASE') + user_id = os.environ.get('TEST_USER_ID') + stop_tracking(channel_id, channel_name, case, user_id) + stop_tracking(channel_id, channel_name, case, user_id) diff --git a/tools/google-cloud-support-slackbot/support_add_comment.py b/tools/google-cloud-support-slackbot/support_add_comment.py new file mode 100755 index 0000000000..b90b612a5e --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_add_comment.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import requests +import logging +from datetime import datetime +from get_parent import get_parent +from case_not_found import case_not_found +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +def support_add_comment(channel_id, case, comment, user_id, user_name): + """ + Add a comment to a Google Cloud support case. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + comment : str + comment to be added to the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + user_name : str + Slack user_name of the user that ran the command. Appended to the end of the + comment to identify who submitted submitted it, otherwise all comments will + show as coming from the case creator + """ + API_KEY = os.environ.get('API_KEY') + MAX_RETRIES = 3 + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ...") + parent = get_parent(case) + + if parent == 'Case not found': + case_not_found(channel_id, user_id, case) + else: + req_body = { + "body": (comment + '\n*Comment submitted by {} via Google Cloud Support' + ' Slack bot*'.format(user_name)) + } + req = support_service.cases().comments().create(parent=parent, body=req_body) + try: + req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your comment may not have posted. Please try again later.") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You added a new comment on case {case}: {comment}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = 'xxxxxxxx' + comment = "This is a test comment created by the Google Cloud Support Slackbot" + user_id = os.environ.get('TEST_USER_ID') + user_name = os.environ.get('TEST_USER_NAME') + support_add_comment(channel_id, case, comment, user_id, user_name) + case = os.environ.get('TEST_CASE') + support_add_comment(channel_id, case, comment, user_id, user_name) diff --git a/tools/google-cloud-support-slackbot/support_change_priority.py b/tools/google-cloud-support-slackbot/support_change_priority.py new file mode 100755 index 0000000000..eed7882fa9 --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_change_priority.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import requests +import logging +from datetime import datetime +from get_parent import get_parent +from case_not_found import case_not_found +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +def support_change_priority(channel_id, case, priority, user_id): + """ + Changes the priority of a Google Cloud Support case. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + priority : str + the current priority of the case, represented as S0, S1, S2, S3, or S4 + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + API_KEY = os.environ.get('API_KEY') + MAX_RETRIES = 3 + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ...") + parent = get_parent(case) + if parent == 'Case not found': + case_not_found(channel_id, user_id, case) + else: + body = { + "severity": priority.replace("P", "S") + } + update_mask = "case.severity" + req = support_service.cases().patch(name=parent, updateMask=update_mask, body=body) + try: + req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your attempt to change the case priority has failed." + " Please try again later.") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You have changed the priority of case {case} to {priority}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = 'xxxxxxxx' + priority = "S3" + user_id = os.environ.get('TEST_USER_ID') + support_change_priority(channel_id, case, priority, user_id) + case = os.environ.get('TEST_CASE') + support_change_priority(channel_id, case, priority, user_id) diff --git a/tools/google-cloud-support-slackbot/support_close_case.py b/tools/google-cloud-support-slackbot/support_close_case.py new file mode 100755 index 0000000000..35a4c12aca --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_close_case.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import requests +import logging +from datetime import datetime +from get_parent import get_parent +from case_not_found import case_not_found +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +def support_close_case(channel_id, case, user_id): + """ + Add a comment to a Google Cloud support case. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + API_KEY = os.environ.get('API_KEY') + MAX_RETRIES = 3 + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ...") + parent = get_parent(case) + + if parent == 'Case not found': + case_not_found(channel_id, user_id, case) + else: + req = support_service.cases().close(name=parent) + try: + req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your case may not have closed. Please try again later.") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You closed case {case}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = 'xxxxxxxx' + user_id = os.environ.get('TEST_USER_ID') + support_close_case(channel_id, case, user_id) + case = os.environ.get('TEST_CASE') + support_close_case(channel_id, case, user_id) diff --git a/tools/google-cloud-support-slackbot/support_create_case.py b/tools/google-cloud-support-slackbot/support_create_case.py new file mode 100755 index 0000000000..ebe04405f3 --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_create_case.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import requests +import logging +from datetime import datetime +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +def support_create_case(channel_id, user_id, user_name, display_name, description, + severity, classification_id, classification_display_name, + time_zone, project_number, test_case) -> str: + """ + Creates a support case. This is meant for automated testing purposes as implementing + this method in Slack would allow individuals to open cases in projects they don't have + access to. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + user_name : str + Slack user_name of the user that ran the command. Appended to the end of the + justification to identify who submitted the escalation, otherwise all escalations + will show as coming from the case creator + display_name : str + title for our case + description : str + description of our case + severity : int + the current priority of the case, represented as 1, 2, 3, 4 + classification_id : str + unique id of the classification object + classification_display_name : str + details the category, component, and subcomponent + time_zone : str + the user's timezone + project_number : str + the unique project number + test_case : bool + flag for support to know if this is a test case + + Returns + ------- + case + unique id of the case + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + MAX_RETRIES = 3 + API_KEY = os.environ.get('API_KEY') + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ... ") + + signed_description = (description + '\n *Sent by {} via Google Cloud Support' + ' Slack bot'.format(user_name)) + body = { + 'display_name': display_name, + 'description': signed_description, + 'severity': severity, + 'classification': { + 'id': '100H41Q3DTMN0TBKCKD0SGRFDLO7AT35412MSPR9DPII4229DPPN8OBECDIG', + 'displayName': 'Compute \u003e Compute Engine \u003e Instance' + }, + 'time_zone': time_zone, + 'testCase': True + } + resource_name = 'projects/' + project_number + req = support_service.cases().create(parent=resource_name, body=body) + try: + resp = req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your attempt to create a case may have failed. Please contact your" + " account team or try again later.") + else: + case = resp['name'].split('/')[-1] + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You have created case {case}") + return case + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + user_id = os.environ.get('TEST_USER_ID') + user_name = os.environ.get('TEST_USER_NAME') + display_name = 'IGNORE -- Google Cloud Support Slackbot test' + description = ('This is an automatically case created by the Google Cloud' + ' Support Slackbot. Please delete this case if it is open for' + ' more than 30 minutes') + severity = 4 + classification_id = '100H41Q3DTMN0TBKCKD0SGRFDLO7AT35412MSPR9DPII4229DPPN8OBECDIG' + classification_display_name = 'Compute \u003e Compute Engine \u003e Instance' + time_zone = '-7:00' + project_number = os.environ.get('TEST_PROJECT_NUMBER') + test_case = True + print(support_create_case(channel_id, user_id, user_name, display_name, description, + severity, classification_id, classification_display_name, time_zone, + project_number, test_case)) diff --git a/tools/google-cloud-support-slackbot/support_escalate.py b/tools/google-cloud-support-slackbot/support_escalate.py new file mode 100755 index 0000000000..5e810d79be --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_escalate.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +""" +Please do not include support_escalate in any tests! Escalations page the +support team (even if the case is flagged as a test case) and the ask is that +we avoid flooding them with false alarms. +""" +import os +import slack +import requests +import logging +from datetime import datetime +from get_parent import get_parent +from case_not_found import case_not_found +from googleapiclient.discovery import build_from_document + +logger = logging.getLogger(__name__) + + +def support_escalate(channel_id, case, user_id, reason, justification, user_name): + """ + Escalates a Google Cloud support case, setting the escalated boolean to True. + This code is currently disabled and we will look to include a working version + of it in the v1 release of the bot. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + reason : str + reason for the escalation. Must be a value of either RESOLUTION_TIME, + TECHNICAL_EXPERTISE, or BUSINESS_IMPACT + justification : str + user submitted string justifying the need for an escalation + user_name : str + Slack user_name of the user that ran the command. Appended to the end of the + justification to identify who submitted the escalation, otherwise all escalations + will show as coming from the case creator + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + MAX_RETRIES = 3 + API_KEY = os.environ.get('API_KEY') + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ... ") + parent = get_parent(case) + if parent == 'Case not found': + case_not_found(channel_id, user_id, case) + else: + signed_justification = (justification + '\n *Sent by {} via Google Cloud Support' + ' Slack bot'.format(user_name)) + body = { + 'escalation': { + 'reason': reason, + 'justification': signed_justification + } + } + req = support_service.cases().escalate(name=parent, body=body) + try: + req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your attempt to escalate may have failed. Please contact your" + " account team or try again later.") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You have escalated case {case}") + + +if __name__ == "__main__": + # Please only test this functionality if the command is failing in production + are_you_sure_about_that = False + if are_you_sure_about_that: + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = os.environ.get('TEST_CASE') + user_id = os.environ.get('TEST_USER_ID') + reason = 'BUSINESS_IMPACT' + justification = ('Please ignore this escalation! This escalation was made for ' + 'testing of the Google Cloud Support Slackbot functionalities.') + user_name = 'Slackbot Admin' + support_escalate(channel_id, case, user_id, reason, justification, user_name) diff --git a/tools/google-cloud-support-slackbot/support_subscribe_email.py b/tools/google-cloud-support-slackbot/support_subscribe_email.py new file mode 100755 index 0000000000..979c6a80c8 --- /dev/null +++ b/tools/google-cloud-support-slackbot/support_subscribe_email.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import requests +import logging +from datetime import datetime +from get_firestore_cases import get_firestore_cases +from case_not_found import case_not_found +from googleapiclient.discovery import build_from_document +from googleapiclient.errors import HttpError + +logger = logging.getLogger(__name__) + + +def support_subscribe_email(channel_id, case, emails, user_id): + """ + Changes the priority of a Google Cloud Support case. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + case : str + unique id of the case + emails : str + emails to be subscribed to the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + API_KEY = os.environ.get('API_KEY') + MAX_RETRIES = 3 + + # Get our discovery doc and build our service + r = requests.get('https://cloudsupport.googleapis.com/$discovery/rest' + '?key={}&labels=V2_TRUSTED_TESTER&version=v2beta'.format(API_KEY)) + r.raise_for_status() + support_service = build_from_document(r.json()) + + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your request is processing ...") + + cases = get_firestore_cases() + case_found = False + + for fs_case in cases: + if case == fs_case['case_number']: + case_found = True + parent = fs_case['resource_name'] + body = { + "subscriberEmailAddresses": [emails] + } + update_mask = 'case.subscriberEmailAddresses' + req = support_service.cases().patch(name=parent, updateMask=update_mask, body=body) + try: + req.execute(num_retries=MAX_RETRIES) + except BrokenPipeError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your attempt to change the subscriber email addresses has failed." + " Please try again later.") + except HttpError as e: + error_message = str(e) + ' : {}'.format(datetime.now()) + logger.error(error_message) + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="Your attempt to change the subscriber email addresses has failed." + " Please confirm that 'Enable case sharing' is on in your" + " project's Support settings. If this setting was off, then for" + " this case you will need to ask support to add the email" + " addresses.") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"You have updated the subcscriber email addreses to {emails}") + + if case_found is False: + case_not_found(channel_id, user_id, case) + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + case = 'xxxxxxxx' + emails = ["testaccount1@example.com", "testaccount2@example.com"] + user_id = os.environ.get('TEST_USER_ID') + support_subscribe_email(channel_id, case, emails, user_id) + case = os.environ.get('TEST_CASE') + support_subscribe_email(channel_id, case, emails, user_id) diff --git a/tools/google-cloud-support-slackbot/track_case.py b/tools/google-cloud-support-slackbot/track_case.py new file mode 100755 index 0000000000..0f95c26c94 --- /dev/null +++ b/tools/google-cloud-support-slackbot/track_case.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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. + +import os +import slack +import logging +from get_parent import get_parent +from case_not_found import case_not_found +from get_firestore_tracked_cases import get_firestore_tracked_cases +from firestore_write import firestore_write + +logger = logging.getLogger(__name__) + + +def track_case(channel_id, channel_name, case, user_id): + """ + Add a Google Cloud support case to the tracked_cases collection in Firestore. If the + case can't be found in the list of active support cases, notify the user. + + Parameters + ---------- + channel_id : str + unique string used to idenify a Slack channel. Used to send messages to the channel + channel_name : str + designated channel name of the channel. For users to understand where their + cases are being tracked in Slack + case : str + unique id of the case + user_id : str + the Slack user_id of the user who submitted the request. Used to send ephemeral + messages to the user + """ + client = slack.WebClient(token=os.environ.get('SLACK_TOKEN')) + collection = 'tracked_cases' + parent = get_parent(case) + tracked_cases = get_firestore_tracked_cases() + + if parent == 'Case not found': + case_not_found(channel_id, user_id, case) + else: + tracker = { + "channel_id": channel_id, + "case": case, + "channel_name": channel_name + } + + exists = False + + for tracked_case in tracked_cases: + tc = tracked_case + if tc['channel_id'] == channel_id and tc['case'] == case: + exists = True + break + + if exists is False: + firestore_write(collection, tracker) + client.chat_postMessage( + channel=channel_id, + text=f"{channel_name} is now tracking case {case}") + else: + client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text=f"Case {case} is already being tracked in {channel_name}") + + +if __name__ == "__main__": + channel_id = os.environ.get('TEST_CHANNEL_ID') + channel_name = os.environ.get('TEST_CHANNEL_NAME') + case = os.environ.get('TEST_CASE') + user_id = os.environ.get('TEST_USER_ID') + track_case(channel_id, channel_name, case, user_id) + track_case(channel_id, channel_name, case, user_id) From 507bc1eac7b5e5b20647e8458f5e22103db4e32a Mon Sep 17 00:00:00 2001 From: TheLanceLord Date: Wed, 23 Feb 2022 12:22:02 -0800 Subject: [PATCH 3/6] README fix for code blocks --- tools/google-cloud-support-slackbot/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/google-cloud-support-slackbot/README.md b/tools/google-cloud-support-slackbot/README.md index 18e5e0c90e..54a4b9a478 100644 --- a/tools/google-cloud-support-slackbot/README.md +++ b/tools/google-cloud-support-slackbot/README.md @@ -31,7 +31,7 @@ Go to [Slack Apps](http://api.slack.com/apps) to do the following: 1. Click **Create New App** and select **From an app manifest** 2. Select the workspace where you want to add the app and then click **Next** 3. Copy and paste in the following YAML and then click **Next**: -''' +``` display_information: name: Google Cloud Support Bot features: @@ -54,7 +54,7 @@ settings: org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false -''' +``` 4. Click **Create** 5. Under **Settings > Basic Information**, scroll down to **Display Information** and upload the [google_cloud_support_buddy_big.png](google_cloud_sup$ 6. Go to **Settings > Basic Information** and under **Building Apps for Slack > Install your app**, click **Install to Workspace**. On the next screen click **Allow**. You may need Slack admin approval to install the app @@ -69,7 +69,7 @@ Go to [Google Cloud](https://console.cloud.google.com/) to do the following: 2. Click the **Activate Cloud Shell** button to open the Cloud Shell Terminal. Confirm the Cloud Shell is set to the project where you want to host the app. If it isn't, set it using the `gcloud config set project PROJECT_ID` command. Authorize the command if prompted. 3. **WARNING**: Running step 4 will delete the default VPC and its associated firewall rules as they aren't needed by our app when it operates in Cloud Run. If you dont want to do this, delete lines 8-12 in the step 4's code block 4. Update the following code block with your `SIGNING_SECRET` and `SLACK_TOKEN` from **Setup Part 1**, and then run it in your **Cloud Shell**: -''' +``` SIGNING_SECRET=SIGNING_SECRET SLACK_TOKEN=SLACK_TOKEN TAG=2.0 @@ -140,7 +140,7 @@ gcloud run deploy google-cloud-support-slackbot \ --region=us-central1 \ --port=5000 \ --project=$DEVSHELL_PROJECT_ID; -''' +``` This will output a URL. Copy this URL to use in **Setup Part 3**. If you need to find this URL again, you can find it under **Cloud Run** by clicking on the google-cloud-support-slackbot service. You will find the URL near the top of the Service details page ## Setup Part 3 - Slack App From 0cc4386bfb9e9d67568efceb93423954ac9edb2e Mon Sep 17 00:00:00 2001 From: TheLanceLord Date: Wed, 23 Feb 2022 12:24:16 -0800 Subject: [PATCH 4/6] Final README fixes --- tools/google-cloud-support-slackbot/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/google-cloud-support-slackbot/README.md b/tools/google-cloud-support-slackbot/README.md index 54a4b9a478..adc3fdbc51 100644 --- a/tools/google-cloud-support-slackbot/README.md +++ b/tools/google-cloud-support-slackbot/README.md @@ -141,14 +141,14 @@ gcloud run deploy google-cloud-support-slackbot \ --port=5000 \ --project=$DEVSHELL_PROJECT_ID; ``` -This will output a URL. Copy this URL to use in **Setup Part 3**. If you need to find this URL again, you can find it under **Cloud Run** by clicking on the google-cloud-support-slackbot service. You will find the URL near the top of the Service details page +This will output a URL. Copy this URL to use in **Setup Part 3**. If you need to find this URL again, you can find it under **Cloud Run** by clicking on the **google-cloud-support-slackbot** service. You will find the URL near the top of the Service details page ## Setup Part 3 - Slack App Return to [Slack Apps](http://api.slack.com/apps) to do the following: 1. Go to **Features > Slash Commands** and click the **pencil icon**: - 1. Update the `Request URL`'s `CLOUDRUN_SERVICE_URL` placeholder with the url generated in Setup Part 2 and then click **Save** + 1. Update the `Request URL`'s `CLOUDRUN_SERVICE_URL` placeholder with the url generated in **Setup Part 2** and then click **Save** ## Testing From 24a12510c91d873d8828dab52686fc91936f4223 Mon Sep 17 00:00:00 2001 From: TheLanceLord Date: Wed, 23 Feb 2022 13:06:36 -0800 Subject: [PATCH 5/6] updating .dockerignore --- tools/google-cloud-support-slackbot/.dockerignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/google-cloud-support-slackbot/.dockerignore b/tools/google-cloud-support-slackbot/.dockerignore index 5511566f81..d484bdda73 100755 --- a/tools/google-cloud-support-slackbot/.dockerignore +++ b/tools/google-cloud-support-slackbot/.dockerignore @@ -5,3 +5,7 @@ freeze.txt .dockerignore archive/* archive +google_cloud_support_slackbot_icon_big.png +google_cloud_support_slackbot_icon_small.png +google_cloud_support_slackbot_icon.svg +README.md From 26b11c60f15ba83159977a3f3d08d302d28a8a86 Mon Sep 17 00:00:00 2001 From: TheLanceLord Date: Thu, 24 Feb 2022 13:21:36 -0800 Subject: [PATCH 6/6] Updated Dockerfile and changed SupportCase.py to support_case.py --- .../google-cloud-support-slackbot/Dockerfile | 36 +++++++++---------- .../case_updates.py | 2 +- .../{SupportCase.py => support_case.py} | 0 3 files changed, 19 insertions(+), 19 deletions(-) rename tools/google-cloud-support-slackbot/{SupportCase.py => support_case.py} (100%) diff --git a/tools/google-cloud-support-slackbot/Dockerfile b/tools/google-cloud-support-slackbot/Dockerfile index 29ba3b2113..f07def4633 100755 --- a/tools/google-cloud-support-slackbot/Dockerfile +++ b/tools/google-cloud-support-slackbot/Dockerfile @@ -12,11 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM debian:10 +FROM python:3.7-slim AS compile-image +LABEL author="Damian Lance" +LABEL created="2022-01-19" +LABEL last_updated="2022-01-19" -LABEL author="Damian Lance" -LABEL created="2022-01-19" -LABEL last_updated="2022-01-19" +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY . ./ +RUN apt-get update; \ + apt-get -y install \ + libffi-dev \ + libssl-dev; \ + pip3 install -r requirements.txt; + +FROM python:3.7-slim AS build-image # Essential environment variables ENV ORG_ID="YOUR NUMERIC ORG ID" @@ -32,19 +43,8 @@ ENV TEST_USER_NAME="SLACK USER_NAME FOR TESTING" ENV PROJECT_ID="GOOGLE CLOUD PROJECT ID FOR SUPPORT CASES" ENV PROJECT_NUMBER="GOOGLE CLOUD PROJECT NUMBER FOR SUPPORT CASES" -WORKDIR /google-cloud-support-slackbot - -COPY . ./ - -RUN apt-get update; \ - apt-get -y install \ - python3-pip \ - curl \ - libffi-dev \ - libssl-dev; \ - pip3 install -r requirements.txt; - -EXPOSE 80 +COPY --from=compile-image . ./ +ENV PATH="/opt/venv/bin:$PATH" -ENTRYPOINT /google-cloud-support-slackbot/main.py +ENTRYPOINT /main.py diff --git a/tools/google-cloud-support-slackbot/case_updates.py b/tools/google-cloud-support-slackbot/case_updates.py index 4d4ec72749..f1fd2e2e14 100755 --- a/tools/google-cloud-support-slackbot/case_updates.py +++ b/tools/google-cloud-support-slackbot/case_updates.py @@ -25,7 +25,7 @@ from get_firestore_first_in import get_firestore_first_in from firestore_delete_cases import firestore_delete_cases from notify_slack import notify_slack -from SupportCase import SupportCase +from support_case import SupportCase logger = logging.getLogger(__name__) diff --git a/tools/google-cloud-support-slackbot/SupportCase.py b/tools/google-cloud-support-slackbot/support_case.py similarity index 100% rename from tools/google-cloud-support-slackbot/SupportCase.py rename to tools/google-cloud-support-slackbot/support_case.py