Skip to content

Commit eb65a91

Browse files
committed
Add Workspace provisioning using Channel API examples
Fixes GoogleCloudPlatform#5583
1 parent 8b72c12 commit eb65a91

File tree

6 files changed

+374
-1
lines changed

6 files changed

+374
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Google Workspace Provisioning codelab.
2+
3+
Instructions for this codelab can be found on this page:
4+
https://cloud.google.com/channel/docs/codelabs/workspace/provisioning
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START_EXCLUDE]
16+
"""Google Workspace Provisioning codelab.
17+
18+
Instructions for this codelab can be found on this page:
19+
https://cloud.google.com/channel/docs/codelabs/workspace/provisioning
20+
"""
21+
# [END_EXCLUDE]
22+
23+
import argparse
24+
25+
from google.cloud import channel
26+
from google.cloud.channel_v1 import types
27+
from google.cloud.channel_v1.services.cloud_channel_service.client import CloudChannelServiceClient
28+
from google.oauth2 import service_account
29+
from google.protobuf.any_pb2 import Any
30+
31+
# The maximum duration in seconds for RPCs to wait before timing out
32+
TIMEOUT = 60
33+
34+
35+
def main(account_name: str, admin_user: str, customer_domain: str, key_file: str) -> None:
36+
client = create_client(admin_user, key_file)
37+
38+
offer = select_offer(client, account_name)
39+
40+
check_exists(client, account_name, customer_domain)
41+
42+
customer = create_customer(client, account_name, customer_domain)
43+
44+
entitlement = create_entitlement(client, customer, offer)
45+
46+
# [START getAdminSDKCustomerId]
47+
customer_id = customer.cloud_identity_id
48+
print(customer_id)
49+
# [END getAdminSDKCustomerId]
50+
51+
suspend_entitlement(client, entitlement)
52+
53+
transfer_entitlement(client, customer, entitlement)
54+
55+
delete_customer(client, customer)
56+
57+
58+
def create_client(admin_user: str, key_file: str) -> CloudChannelServiceClient:
59+
"""Creates the Channel Service API client
60+
61+
Returns:
62+
The created Channel Service API client
63+
"""
64+
# [START createClient]
65+
66+
# Set up credentials with user impersonation
67+
credentials = service_account.Credentials.from_service_account_file(
68+
key_file, scopes=["https://www.googleapis.com/auth/apps.order"])
69+
credentials_delegated = credentials.with_subject(admin_user)
70+
71+
# Create the API client
72+
client = channel.CloudChannelServiceClient(credentials=credentials_delegated)
73+
74+
print("=== Created client")
75+
# [END createClient]
76+
77+
return client
78+
79+
80+
def select_offer(client: CloudChannelServiceClient, account_name: str) -> types.offers.Offer:
81+
"""Selects a Workspace offer.
82+
83+
Returns:
84+
A Channel API Offer for Workspace
85+
"""
86+
# [START selectOffer]
87+
request = channel.ListOffersRequest(parent=account_name)
88+
offers = client.list_offers(request)
89+
90+
# For the purpose of this codelab, the code lists all offers and selects
91+
# the first offer for Google Workspace Business Standard on an Annual
92+
# plan. This is needed because offerIds vary from one account to another,
93+
# but this is not a recommended model for your production integration
94+
sample_offer = "Google Workspace Business Standard"
95+
sample_plan = types.offers.PaymentPlan.COMMITMENT
96+
selected_offer = None
97+
for offer in offers:
98+
if offer.sku.marketing_info.display_name == sample_offer and \
99+
offer.plan.payment_plan == sample_plan:
100+
selected_offer = offer
101+
break
102+
103+
print("=== Selected offer")
104+
print(selected_offer)
105+
# [END selectOffer]
106+
107+
return selected_offer
108+
109+
110+
def check_exists(client: CloudChannelServiceClient, account_name: str, customer_domain: str) -> None:
111+
"""Determine if customer already has a cloud identity.
112+
113+
Raises:
114+
Exception: if the domain is already in use
115+
"""
116+
# [START checkExists]
117+
# Determine if customer already has a cloud identity
118+
request = channel.CheckCloudIdentityAccountsExistRequest(
119+
parent=account_name, domain=customer_domain)
120+
121+
response = client.check_cloud_identity_accounts_exist(request)
122+
123+
if response.cloud_identity_accounts:
124+
raise Exception(
125+
"Cloud identity already exists. Customer must be transferred. " +
126+
"Out of scope for this codelab")
127+
# [END checkExists]
128+
129+
130+
def create_customer(client: CloudChannelServiceClient, account_name: str, customer_domain: str) -> Any:
131+
"""Create the Customer resource, with a cloud identity.
132+
133+
Args:
134+
customer_domain: primary domain used by the customer]
135+
136+
Returns:
137+
The created Channel API Customer
138+
"""
139+
# [START createCustomer]
140+
# Create the Customer resource
141+
request = channel.CreateCustomerRequest(
142+
parent=account_name,
143+
customer={
144+
"org_display_name": "Acme Corp",
145+
"domain": customer_domain,
146+
"org_postal_address": {
147+
"address_lines": ["1800 Amphibious Blvd"],
148+
"postal_code": "94045",
149+
"region_code": "US"
150+
}
151+
})
152+
# Distributors need to also pass the following field for the `customer`
153+
# "channel_partner_id": channel_partner_link_id
154+
155+
customer = client.create_customer(request)
156+
157+
print("=== Created customer")
158+
print(customer)
159+
# [END createCustomer]
160+
161+
# [START provisionCloudIdentity]
162+
cloud_identity_info = channel.CloudIdentityInfo(
163+
alternate_email="john.doe@gmail.com", language_code="en-US")
164+
165+
admin_user = channel.AdminUser(
166+
given_name="John", family_name="Doe", email="admin@" + customer_domain)
167+
168+
cloud_identity_request = channel.ProvisionCloudIdentityRequest(
169+
customer=customer.name,
170+
cloud_identity_info=cloud_identity_info,
171+
user=admin_user)
172+
173+
# This call returns a long-running operation.
174+
operation = client.provision_cloud_identity(cloud_identity_request)
175+
176+
# Wait for the long-running operation and get the result.
177+
customer = operation.result(TIMEOUT)
178+
179+
print("=== Provisioned cloud identity")
180+
# [END provisionCloudIdentity]
181+
182+
return customer
183+
184+
185+
def create_entitlement(client: CloudChannelServiceClient, customer: types.customers.Customer, selected_offer: types.offers.Offer) -> Any:
186+
"""Create the Channel API Entitlement.
187+
188+
Args:
189+
customer: a Customer resource
190+
selected_offer: an Offer
191+
192+
Returns:
193+
The created Entitlement
194+
"""
195+
# [START createEntitlement]
196+
request = channel.CreateEntitlementRequest(
197+
parent=customer.name,
198+
entitlement={
199+
"offer": selected_offer.name,
200+
# Setting 5 seats for this Annual offer
201+
"parameters": [{
202+
"name": "num_units",
203+
"value": {
204+
"int64_value": 5
205+
}
206+
}],
207+
"commitment_settings": {
208+
"renewal_settings": {
209+
# Setting renewal settings to auto renew
210+
"enable_renewal": True,
211+
"payment_plan": "COMMITMENT",
212+
"payment_cycle": {
213+
"period_type": "YEAR",
214+
"duration": 1
215+
}
216+
}
217+
},
218+
# A string of up to 80 characters.
219+
# We recommend an internal transaction ID or
220+
# identifier for this customer in this field.
221+
"purchase_order_id": "A codelab test"
222+
})
223+
224+
# This call returns a long-running operation.
225+
operation = client.create_entitlement(request)
226+
227+
# Wait for the long-running operation and get the result.
228+
entitlement = operation.result(TIMEOUT)
229+
230+
print("=== Created entitlement")
231+
print(entitlement)
232+
# [END createEntitlement]
233+
234+
return entitlement
235+
236+
237+
def suspend_entitlement(client: CloudChannelServiceClient, entitlement: types.entitlements.Entitlement) -> Any:
238+
"""Suspend the Channel API Entitlement.
239+
240+
Args:
241+
entitlement: an Entitlement to suspend
242+
243+
Returns:
244+
The suspended Entitlement
245+
"""
246+
# [START suspendEntitlement]
247+
request = channel.SuspendEntitlementRequest(name=entitlement.name)
248+
249+
# This call returns a long-running operation.
250+
operation = client.suspend_entitlement(request)
251+
252+
# Wait for the long-running operation and get the result.
253+
result = operation.result(TIMEOUT)
254+
255+
print("=== Suspended entitlement")
256+
print(result)
257+
# [END suspendEntitlement]
258+
259+
return result
260+
261+
262+
def transfer_entitlement(client: CloudChannelServiceClient, customer: types.customers.Customer, entitlement: types.entitlements.Entitlement) -> Any:
263+
"""Transfer the Channel API Entitlement to Google.
264+
265+
Args:
266+
entitlement: an Entitlement to transfer
267+
268+
Returns:
269+
google.protobuf.Empty on success
270+
"""
271+
# [START transferEntitlement]
272+
request = channel.TransferEntitlementsToGoogleRequest(
273+
parent=customer.name,
274+
entitlements=[entitlement])
275+
276+
# This call returns a long-running operation.
277+
operation = client.transfer_entitlements_to_google(request)
278+
279+
# Wait for the long-running operation and get the result.
280+
result = operation.result(TIMEOUT)
281+
282+
print("=== Transfered entitlement")
283+
print(result)
284+
# [END transferEntitlement]
285+
286+
return result
287+
288+
289+
def delete_customer(client: CloudChannelServiceClient, customer: types.customers.Customer) -> None:
290+
"""Delete the Customer.
291+
292+
Args:
293+
customer: a Customer to delete
294+
"""
295+
# [START deleteCustomer]
296+
request = channel.DeleteCustomerRequest(name=customer.name)
297+
298+
client.delete_customer(request)
299+
300+
print("=== Deleted customer")
301+
# [END deleteCustomer]
302+
303+
304+
if __name__ == "__main__":
305+
parser = argparse.ArgumentParser(
306+
description=__doc__,
307+
formatter_class=argparse.RawDescriptionHelpFormatter)
308+
parser.add_argument('--account_name', required=True, help='The resource name of the reseller account. Format: accounts/{account_id}.')
309+
parser.add_argument('--admin_user', required=True, help='The email address of a reseller domain super admin (preferably of your Test Channel Services Console).')
310+
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.')
311+
parser.add_argument('--key_file', required=True, help='The path to the JSON key file generated when you created a service account.')
312+
313+
args = parser.parse_args()
314+
315+
main(args.account_name, args.admin_user, args.customer_domain, args.key_file)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import uuid
17+
18+
import pytest
19+
20+
from create_entitlement import main
21+
22+
ACCOUNT_ID = os.environ['CHANNEL_RESELLER_ACCOUNT_ID']
23+
ADMIN_USER = os.environ['CHANNEL_RESELLER_ADMIN_USER']
24+
PARENT_DOMAIN = os.environ['CHANNEL_CUSTOMER_PARENT_DOMAIN']
25+
KEY_FILE = os.environ['CHANNEL_JSON_KEY_FILE']
26+
27+
28+
@pytest.mark.flaky(max_runs=3, min_passes=1)
29+
def test_main(capsys: pytest.CaptureFixture) -> None:
30+
account_name = "accounts/" + ACCOUNT_ID
31+
customer_domain = f'goog-test.ci.{uuid.uuid4().hex}.{PARENT_DOMAIN}'
32+
main(account_name, ADMIN_USER, customer_domain, KEY_FILE)
33+
34+
out, _ = capsys.readouterr()
35+
36+
assert "=== Created client" in out
37+
assert "=== Selected offer" in out
38+
assert "=== Created customer" in out
39+
assert "=== Provisioned cloud identity" in out
40+
assert "=== Created entitlement" in out
41+
assert "=== Suspended entitlement" in out
42+
assert "=== Transfered entitlement" in out
43+
assert "=== Deleted customer" in out
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
flaky==3.7.0
2+
pytest==6.2.2
3+
uuid==1.30
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
argparse==1.4.0
2+
google-cloud-channel==0.2.0

testing/test-env.tmpl.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,10 @@ export IDP_KEY=
106106

107107
# Dialogflow examples.
108108
export SMART_REPLY_MODEL=
109-
export SMART_REPLY_ALLOWLIST=
109+
export SMART_REPLY_ALLOWLIST=
110+
111+
# Channel Services examples
112+
export CHANNEL_RESELLER_ACCOUNT_ID=
113+
export CHANNEL_RESELLER_ADMIN_USER=
114+
export CHANNEL_CUSTOMER_PARENT_DOMAIN=
115+
export CHANNEL_JSON_KEY_FILE=

0 commit comments

Comments
 (0)