From eb65a91c4d1dfb65ae861bd4741cfab1e474f1d0 Mon Sep 17 00:00:00 2001 From: Evgenii Vais Date: Mon, 29 Mar 2021 16:49:19 -0700 Subject: [PATCH 1/2] Add Workspace provisioning using Channel API examples Fixes #5583 --- channel/workspace/provisioning/README.md | 4 + .../provisioning/create_entitlement.py | 315 ++++++++++++++++++ .../provisioning/create_entitlement_test.py | 43 +++ .../provisioning/requirements-test.txt | 3 + .../workspace/provisioning/requirements.txt | 2 + testing/test-env.tmpl.sh | 8 +- 6 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 channel/workspace/provisioning/README.md create mode 100644 channel/workspace/provisioning/create_entitlement.py create mode 100644 channel/workspace/provisioning/create_entitlement_test.py create mode 100644 channel/workspace/provisioning/requirements-test.txt create mode 100644 channel/workspace/provisioning/requirements.txt diff --git a/channel/workspace/provisioning/README.md b/channel/workspace/provisioning/README.md new file mode 100644 index 00000000000..01e66798ece --- /dev/null +++ b/channel/workspace/provisioning/README.md @@ -0,0 +1,4 @@ +# Google Workspace Provisioning codelab. + +Instructions for this codelab can be found on this page: +https://cloud.google.com/channel/docs/codelabs/workspace/provisioning \ No newline at end of file diff --git a/channel/workspace/provisioning/create_entitlement.py b/channel/workspace/provisioning/create_entitlement.py new file mode 100644 index 00000000000..4496de24069 --- /dev/null +++ b/channel/workspace/provisioning/create_entitlement.py @@ -0,0 +1,315 @@ +# 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. + +# [START_EXCLUDE] +"""Google Workspace Provisioning codelab. + +Instructions for this codelab can be found on this page: +https://cloud.google.com/channel/docs/codelabs/workspace/provisioning +""" +# [END_EXCLUDE] + +import argparse + +from google.cloud import channel +from google.cloud.channel_v1 import types +from google.cloud.channel_v1.services.cloud_channel_service.client import CloudChannelServiceClient +from google.oauth2 import service_account +from google.protobuf.any_pb2 import Any + +# The maximum duration in seconds for RPCs to wait before timing out +TIMEOUT = 60 + + +def main(account_name: str, admin_user: str, customer_domain: str, key_file: str) -> None: + client = create_client(admin_user, key_file) + + offer = select_offer(client, account_name) + + check_exists(client, account_name, customer_domain) + + customer = create_customer(client, account_name, customer_domain) + + entitlement = create_entitlement(client, customer, offer) + + # [START getAdminSDKCustomerId] + customer_id = customer.cloud_identity_id + print(customer_id) + # [END getAdminSDKCustomerId] + + suspend_entitlement(client, entitlement) + + transfer_entitlement(client, customer, entitlement) + + delete_customer(client, customer) + + +def create_client(admin_user: str, key_file: str) -> CloudChannelServiceClient: + """Creates the Channel Service API client + + Returns: + The created Channel Service API client + """ + # [START createClient] + + # Set up credentials with user impersonation + credentials = service_account.Credentials.from_service_account_file( + key_file, scopes=["https://www.googleapis.com/auth/apps.order"]) + credentials_delegated = credentials.with_subject(admin_user) + + # Create the API client + client = channel.CloudChannelServiceClient(credentials=credentials_delegated) + + print("=== Created client") + # [END createClient] + + return client + + +def select_offer(client: CloudChannelServiceClient, account_name: str) -> types.offers.Offer: + """Selects a Workspace offer. + + Returns: + A Channel API Offer for Workspace + """ + # [START selectOffer] + request = channel.ListOffersRequest(parent=account_name) + offers = client.list_offers(request) + + # For the purpose of this codelab, the code lists all offers and selects + # the first offer for Google Workspace Business Standard on an Annual + # plan. This is needed because offerIds vary from one account to another, + # but this is not a recommended model for your production integration + sample_offer = "Google Workspace Business Standard" + sample_plan = types.offers.PaymentPlan.COMMITMENT + selected_offer = None + for offer in offers: + if offer.sku.marketing_info.display_name == sample_offer and \ + offer.plan.payment_plan == sample_plan: + selected_offer = offer + break + + print("=== Selected offer") + print(selected_offer) + # [END selectOffer] + + return selected_offer + + +def check_exists(client: CloudChannelServiceClient, account_name: str, customer_domain: str) -> None: + """Determine if customer already has a cloud identity. + + Raises: + Exception: if the domain is already in use + """ + # [START checkExists] + # Determine if customer already has a cloud identity + request = channel.CheckCloudIdentityAccountsExistRequest( + parent=account_name, domain=customer_domain) + + response = client.check_cloud_identity_accounts_exist(request) + + if response.cloud_identity_accounts: + raise Exception( + "Cloud identity already exists. Customer must be transferred. " + + "Out of scope for this codelab") + # [END checkExists] + + +def create_customer(client: CloudChannelServiceClient, account_name: str, customer_domain: str) -> Any: + """Create the Customer resource, with a cloud identity. + + Args: + customer_domain: primary domain used by the customer] + + Returns: + The created Channel API Customer + """ + # [START createCustomer] + # Create the Customer resource + request = channel.CreateCustomerRequest( + parent=account_name, + customer={ + "org_display_name": "Acme Corp", + "domain": customer_domain, + "org_postal_address": { + "address_lines": ["1800 Amphibious Blvd"], + "postal_code": "94045", + "region_code": "US" + } + }) + # Distributors need to also pass the following field for the `customer` + # "channel_partner_id": channel_partner_link_id + + customer = client.create_customer(request) + + print("=== Created customer") + print(customer) + # [END createCustomer] + + # [START provisionCloudIdentity] + cloud_identity_info = channel.CloudIdentityInfo( + alternate_email="john.doe@gmail.com", language_code="en-US") + + admin_user = channel.AdminUser( + given_name="John", family_name="Doe", email="admin@" + customer_domain) + + cloud_identity_request = channel.ProvisionCloudIdentityRequest( + customer=customer.name, + cloud_identity_info=cloud_identity_info, + user=admin_user) + + # This call returns a long-running operation. + operation = client.provision_cloud_identity(cloud_identity_request) + + # Wait for the long-running operation and get the result. + customer = operation.result(TIMEOUT) + + print("=== Provisioned cloud identity") + # [END provisionCloudIdentity] + + return customer + + +def create_entitlement(client: CloudChannelServiceClient, customer: types.customers.Customer, selected_offer: types.offers.Offer) -> Any: + """Create the Channel API Entitlement. + + Args: + customer: a Customer resource + selected_offer: an Offer + + Returns: + The created Entitlement + """ + # [START createEntitlement] + request = channel.CreateEntitlementRequest( + parent=customer.name, + entitlement={ + "offer": selected_offer.name, + # Setting 5 seats for this Annual offer + "parameters": [{ + "name": "num_units", + "value": { + "int64_value": 5 + } + }], + "commitment_settings": { + "renewal_settings": { + # Setting renewal settings to auto renew + "enable_renewal": True, + "payment_plan": "COMMITMENT", + "payment_cycle": { + "period_type": "YEAR", + "duration": 1 + } + } + }, + # A string of up to 80 characters. + # We recommend an internal transaction ID or + # identifier for this customer in this field. + "purchase_order_id": "A codelab test" + }) + + # This call returns a long-running operation. + operation = client.create_entitlement(request) + + # Wait for the long-running operation and get the result. + entitlement = operation.result(TIMEOUT) + + print("=== Created entitlement") + print(entitlement) + # [END createEntitlement] + + return entitlement + + +def suspend_entitlement(client: CloudChannelServiceClient, entitlement: types.entitlements.Entitlement) -> Any: + """Suspend the Channel API Entitlement. + + Args: + entitlement: an Entitlement to suspend + + Returns: + The suspended Entitlement + """ + # [START suspendEntitlement] + request = channel.SuspendEntitlementRequest(name=entitlement.name) + + # This call returns a long-running operation. + operation = client.suspend_entitlement(request) + + # Wait for the long-running operation and get the result. + result = operation.result(TIMEOUT) + + print("=== Suspended entitlement") + print(result) + # [END suspendEntitlement] + + return result + + +def transfer_entitlement(client: CloudChannelServiceClient, customer: types.customers.Customer, entitlement: types.entitlements.Entitlement) -> Any: + """Transfer the Channel API Entitlement to Google. + + Args: + entitlement: an Entitlement to transfer + + Returns: + google.protobuf.Empty on success + """ + # [START transferEntitlement] + request = channel.TransferEntitlementsToGoogleRequest( + parent=customer.name, + entitlements=[entitlement]) + + # This call returns a long-running operation. + operation = client.transfer_entitlements_to_google(request) + + # Wait for the long-running operation and get the result. + result = operation.result(TIMEOUT) + + print("=== Transfered entitlement") + print(result) + # [END transferEntitlement] + + return result + + +def delete_customer(client: CloudChannelServiceClient, customer: types.customers.Customer) -> None: + """Delete the Customer. + + Args: + customer: a Customer to delete + """ + # [START deleteCustomer] + request = channel.DeleteCustomerRequest(name=customer.name) + + client.delete_customer(request) + + print("=== Deleted customer") + # [END deleteCustomer] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--account_name', required=True, help='The resource name of the reseller account. Format: accounts/{account_id}.') + parser.add_argument('--admin_user', required=True, help='The email address of a reseller domain super admin (preferably of your Test Channel Services Console).') + parser.add_argument('--customer_domain', required=True, help='The end customer''s domain. If you run this codelab on your Test Channel Services Console, make sure the domain follows domain naming conventions.') + parser.add_argument('--key_file', required=True, help='The path to the JSON key file generated when you created a service account.') + + args = parser.parse_args() + + main(args.account_name, args.admin_user, args.customer_domain, args.key_file) diff --git a/channel/workspace/provisioning/create_entitlement_test.py b/channel/workspace/provisioning/create_entitlement_test.py new file mode 100644 index 00000000000..25c8c9ec021 --- /dev/null +++ b/channel/workspace/provisioning/create_entitlement_test.py @@ -0,0 +1,43 @@ +# 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 uuid + +import pytest + +from create_entitlement import main + +ACCOUNT_ID = os.environ['CHANNEL_RESELLER_ACCOUNT_ID'] +ADMIN_USER = os.environ['CHANNEL_RESELLER_ADMIN_USER'] +PARENT_DOMAIN = os.environ['CHANNEL_CUSTOMER_PARENT_DOMAIN'] +KEY_FILE = os.environ['CHANNEL_JSON_KEY_FILE'] + + +@pytest.mark.flaky(max_runs=3, min_passes=1) +def test_main(capsys: pytest.CaptureFixture) -> None: + account_name = "accounts/" + ACCOUNT_ID + customer_domain = f'goog-test.ci.{uuid.uuid4().hex}.{PARENT_DOMAIN}' + main(account_name, ADMIN_USER, customer_domain, KEY_FILE) + + out, _ = capsys.readouterr() + + assert "=== Created client" in out + assert "=== Selected offer" in out + assert "=== Created customer" in out + assert "=== Provisioned cloud identity" in out + assert "=== Created entitlement" in out + assert "=== Suspended entitlement" in out + assert "=== Transfered entitlement" in out + assert "=== Deleted customer" in out diff --git a/channel/workspace/provisioning/requirements-test.txt b/channel/workspace/provisioning/requirements-test.txt new file mode 100644 index 00000000000..8d9d4997284 --- /dev/null +++ b/channel/workspace/provisioning/requirements-test.txt @@ -0,0 +1,3 @@ +flaky==3.7.0 +pytest==6.2.2 +uuid==1.30 \ No newline at end of file diff --git a/channel/workspace/provisioning/requirements.txt b/channel/workspace/provisioning/requirements.txt new file mode 100644 index 00000000000..301bd513dae --- /dev/null +++ b/channel/workspace/provisioning/requirements.txt @@ -0,0 +1,2 @@ +argparse==1.4.0 +google-cloud-channel==0.2.0 \ No newline at end of file diff --git a/testing/test-env.tmpl.sh b/testing/test-env.tmpl.sh index 6e03201a942..ff6b721e299 100644 --- a/testing/test-env.tmpl.sh +++ b/testing/test-env.tmpl.sh @@ -106,4 +106,10 @@ export IDP_KEY= # Dialogflow examples. export SMART_REPLY_MODEL= -export SMART_REPLY_ALLOWLIST= \ No newline at end of file +export SMART_REPLY_ALLOWLIST= + +# Channel Services examples +export CHANNEL_RESELLER_ACCOUNT_ID= +export CHANNEL_RESELLER_ADMIN_USER= +export CHANNEL_CUSTOMER_PARENT_DOMAIN= +export CHANNEL_JSON_KEY_FILE= \ No newline at end of file From fb4a1c8ab169372dc593996a8f9277d8798e1471 Mon Sep 17 00:00:00 2001 From: Evgenii Vais Date: Wed, 31 Mar 2021 10:29:41 -0700 Subject: [PATCH 2/2] Update the region tags style in Channel API samples Refers #5583 accordingly [this](https://github.com/GoogleCloudPlatform/python-docs-samples/pull/5593#issuecomment-811173807) comment. --- .../provisioning/create_entitlement.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/channel/workspace/provisioning/create_entitlement.py b/channel/workspace/provisioning/create_entitlement.py index 4496de24069..c1706e4ce98 100644 --- a/channel/workspace/provisioning/create_entitlement.py +++ b/channel/workspace/provisioning/create_entitlement.py @@ -43,10 +43,10 @@ def main(account_name: str, admin_user: str, customer_domain: str, key_file: str entitlement = create_entitlement(client, customer, offer) - # [START getAdminSDKCustomerId] + # [START channel_get_admin_sdk_customer_id] customer_id = customer.cloud_identity_id print(customer_id) - # [END getAdminSDKCustomerId] + # [END channel_get_admin_sdk_customer_id] suspend_entitlement(client, entitlement) @@ -61,7 +61,7 @@ def create_client(admin_user: str, key_file: str) -> CloudChannelServiceClient: Returns: The created Channel Service API client """ - # [START createClient] + # [START channel_create_client] # Set up credentials with user impersonation credentials = service_account.Credentials.from_service_account_file( @@ -72,7 +72,7 @@ def create_client(admin_user: str, key_file: str) -> CloudChannelServiceClient: client = channel.CloudChannelServiceClient(credentials=credentials_delegated) print("=== Created client") - # [END createClient] + # [END channel_create_client] return client @@ -83,7 +83,7 @@ def select_offer(client: CloudChannelServiceClient, account_name: str) -> types. Returns: A Channel API Offer for Workspace """ - # [START selectOffer] + # [START channel_select_offer] request = channel.ListOffersRequest(parent=account_name) offers = client.list_offers(request) @@ -102,7 +102,7 @@ def select_offer(client: CloudChannelServiceClient, account_name: str) -> types. print("=== Selected offer") print(selected_offer) - # [END selectOffer] + # [END channel_select_offer] return selected_offer @@ -113,7 +113,7 @@ def check_exists(client: CloudChannelServiceClient, account_name: str, customer_ Raises: Exception: if the domain is already in use """ - # [START checkExists] + # [START channel_check_exists] # Determine if customer already has a cloud identity request = channel.CheckCloudIdentityAccountsExistRequest( parent=account_name, domain=customer_domain) @@ -124,7 +124,7 @@ def check_exists(client: CloudChannelServiceClient, account_name: str, customer_ raise Exception( "Cloud identity already exists. Customer must be transferred. " + "Out of scope for this codelab") - # [END checkExists] + # [END channel_check_exists] def create_customer(client: CloudChannelServiceClient, account_name: str, customer_domain: str) -> Any: @@ -136,7 +136,7 @@ def create_customer(client: CloudChannelServiceClient, account_name: str, custom Returns: The created Channel API Customer """ - # [START createCustomer] + # [START channel_create_customer] # Create the Customer resource request = channel.CreateCustomerRequest( parent=account_name, @@ -156,9 +156,9 @@ def create_customer(client: CloudChannelServiceClient, account_name: str, custom print("=== Created customer") print(customer) - # [END createCustomer] + # [END channel_create_customer] - # [START provisionCloudIdentity] + # [START channel_provision_cloud_identity] cloud_identity_info = channel.CloudIdentityInfo( alternate_email="john.doe@gmail.com", language_code="en-US") @@ -177,7 +177,7 @@ def create_customer(client: CloudChannelServiceClient, account_name: str, custom customer = operation.result(TIMEOUT) print("=== Provisioned cloud identity") - # [END provisionCloudIdentity] + # [END channel_provision_cloud_identity] return customer @@ -192,7 +192,7 @@ def create_entitlement(client: CloudChannelServiceClient, customer: types.custom Returns: The created Entitlement """ - # [START createEntitlement] + # [START channel_create_entitlement] request = channel.CreateEntitlementRequest( parent=customer.name, entitlement={ @@ -229,7 +229,7 @@ def create_entitlement(client: CloudChannelServiceClient, customer: types.custom print("=== Created entitlement") print(entitlement) - # [END createEntitlement] + # [END channel_create_entitlement] return entitlement @@ -243,7 +243,7 @@ def suspend_entitlement(client: CloudChannelServiceClient, entitlement: types.en Returns: The suspended Entitlement """ - # [START suspendEntitlement] + # [START channel_suspend_entitlement] request = channel.SuspendEntitlementRequest(name=entitlement.name) # This call returns a long-running operation. @@ -254,7 +254,7 @@ def suspend_entitlement(client: CloudChannelServiceClient, entitlement: types.en print("=== Suspended entitlement") print(result) - # [END suspendEntitlement] + # [END channel_suspend_entitlement] return result @@ -268,7 +268,7 @@ def transfer_entitlement(client: CloudChannelServiceClient, customer: types.cust Returns: google.protobuf.Empty on success """ - # [START transferEntitlement] + # [START channel_transfer_entitlement] request = channel.TransferEntitlementsToGoogleRequest( parent=customer.name, entitlements=[entitlement]) @@ -281,7 +281,7 @@ def transfer_entitlement(client: CloudChannelServiceClient, customer: types.cust print("=== Transfered entitlement") print(result) - # [END transferEntitlement] + # [END channel_transfer_entitlement] return result @@ -292,13 +292,13 @@ def delete_customer(client: CloudChannelServiceClient, customer: types.customers Args: customer: a Customer to delete """ - # [START deleteCustomer] + # [START channel_delete_customer] request = channel.DeleteCustomerRequest(name=customer.name) client.delete_customer(request) print("=== Deleted customer") - # [END deleteCustomer] + # [END channel_delete_customer] if __name__ == "__main__":