Skip to content

Commit f89e9fd

Browse files
Merge pull request #32 from solace-iot-team/feature-au
Feature au
2 parents 9d9aab6 + c719674 commit f89e9fd

39 files changed

+809
-52
lines changed

.github/workflows/ansible-test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
fail-fast: false
3232
matrix:
3333
# 3.6 is always tested
34-
python-version: [ '3.8' ]
34+
python-version: [ '3.8.10' ]
3535
ansible-version: [
3636
"ansible>=2.10.3,<2.11", # this is 2.10
3737
"ansible>=4.10.0,<5.0.0", # this is 2.11

.github/workflows/integration.yml

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ jobs:
151151
export PROJECT_HOME="$GITHUB_WORKSPACE"
152152
export SOLACE_CLOUD_API_TOKEN_ALL_PERMISSIONS="${{ secrets.SOLACE_CLOUD_API_TOKEN_ALL_PERMISSIONS }}"
153153
export SOLACE_CLOUD_API_TOKEN_RESTRICTED_PERMISSIONS="${{ secrets.SOLACE_CLOUD_API_TOKEN_RESTRICTED_PERMISSIONS }}"
154+
export SOLACE_CLOUD_API_TOKEN_US="${{ secrets.SOLACE_CLOUD_API_TOKEN_ALL_PERMISSIONS }}"
155+
export SOLACE_CLOUD_API_TOKEN_AU="${{ secrets.SOLACE_CLOUD_API_TOKEN_ALL_PERMISSIONS_SOLACE_AU }}"
154156
export LOG_DIR="$GITHUB_WORKSPACE/${TEST_RUNNER_LOGS_DIR}"
155157
export AZURE_PROJECT_NAME="asct-wf"
156158
# source $PROJECT_HOME/test-runner/source.env.integration.core.sh

ReleaseNotes.md

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Release Notes
22

3+
## Version 1.9.0
4+
5+
**Framework:**
6+
* **Support for Solace Cloud Home: US & AU**
7+
- affects all modules which use the Solace Cloud Api
8+
- new env var: ANSIBLE_SOLACE_SOLACE_CLOUD_HOME='AU' or 'US'
9+
- new optional module parameter: solace_cloud_home: choices=['us', 'au']
10+
- logic:
11+
- default: 'US'
12+
- if module parameter is set: use module parameter
13+
- else check env var
14+
- if set, all modules which use the Solace Cloud API will switch their base url according to the home cloud
15+
* **Fixed timeout for Solace Cloud Api Post request polling**
16+
- timeout parameter now passed through
17+
- min polling time set to 5 mins
18+
* **Fixed error on outstanding requests for serviceId for Solace Cloud Api**
19+
- check if serviceId has any outstanding requests before submitting a new one
20+
- uses same timeout as the actual request
21+
22+
323
## Version 1.8.0
424
New modules to manage cert authorities & support for ansible_core 2.12.
525

devel/devel.requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pycodestyle
33
pyyaml
44
voluptuous
55
yamllint
6-
rstcheck
6+
rstcheck==3.3.1
77
Sphinx
88
sphinx-rtd-theme
99
sphinxcontrib-contentui

src/ansible_collections/solace/pubsub_plus/docs/source/inventory-files.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Inventory File for a Solace Cloud Account
201201
ansible_connection: local
202202
broker_type: solace_cloud
203203
solace_cloud_api_token: the-token
204-
204+
solace_cloud_home: us
205205
206206
When your Playbook only manages a Solace Cloud account, for example to start/stop services, all you need in the Inventory File is the api token with the correct permissions.
207207

src/ansible_collections/solace/pubsub_plus/docs/source/solace_cloud_modules.rst

+24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ This can either be provided in the inventory file or as an environment variable
66

77
See a detailed discussion here: :ref:`inventory_files_solace_cloud_account`.
88

9+
Setting Solace Cloud Home Cloud Region
10+
--------------------------------------
11+
12+
Ansible Solace currently supports two Home Cloud Regions: `us` & `au`.
13+
14+
You can either use an environment variable to set the home region for all solace cloud modules and/or you can specify an additional parameter module by module.
15+
16+
Environment Variable: `ANSIBLE_SOLACE_SOLACE_CLOUD_HOME` , values: ["US", "AU"].
17+
18+
Module parameter:
19+
20+
* solace_cloud_home: choices=['us', 'au']
21+
22+
The logic of choosing the home region is as follows:
23+
24+
.. code-block:: none
25+
26+
- default: US
27+
- if module parameter solace_cloud_home is set, use value from module parameter
28+
- else
29+
- check if env var is set and use value from env
30+
- else
31+
- use default
32+
933
1034
Module Reference
1135
----------------

src/ansible_collections/solace/pubsub_plus/galaxy.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace: solace
88
name: pubsub_plus
99

1010
# The version of the collection. Must be compatible with semantic versioning
11-
version: 1.8.0
11+
version: 1.9.0
1212

1313
# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
1414
readme: README.md

src/ansible_collections/solace/pubsub_plus/plugins/doc_fragments/solace.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ class ModuleDocFragment(object):
104104

105105
BROKER_CONFIG_SOLACE_CLOUD = r'''
106106
options:
107+
solace_cloud_home:
108+
description: The Solace Cloud home region.
109+
type: str
110+
required: false
111+
choices:
112+
- us
113+
- au
114+
- US
115+
- AU
116+
- ''
107117
solace_cloud_api_token:
108118
description:
109119
- The API Token.
@@ -122,6 +132,16 @@ class ModuleDocFragment(object):
122132

123133
BROKER_CONFIG_SOLACE_CLOUD_MANDATORY = r'''
124134
options:
135+
solace_cloud_home:
136+
description: The Solace Cloud home region.
137+
type: str
138+
required: false
139+
choices:
140+
- us
141+
- au
142+
- US
143+
- AU
144+
- ''
125145
solace_cloud_api_token:
126146
description:
127147
- The API Token.
@@ -140,6 +160,16 @@ class ModuleDocFragment(object):
140160

141161
SOLACE_CLOUD_CONFIG_SOLACE_CLOUD = r'''
142162
options:
163+
solace_cloud_home:
164+
description: The Solace Cloud home region.
165+
type: str
166+
required: false
167+
choices:
168+
- us
169+
- au
170+
- US
171+
- AU
172+
- ''
143173
solace_cloud_api_token:
144174
description:
145175
- The API Token.
@@ -148,7 +178,7 @@ class ModuleDocFragment(object):
148178
required: true
149179
aliases: [api_token]
150180
timeout:
151-
description: Connection timeout in seconds for the http request.
181+
description: Connection timeout in seconds for the http request or overall call interaction timeout for Solace Cloud API.
152182
required: false
153183
default: 60
154184
type: int

src/ansible_collections/solace/pubsub_plus/plugins/module_utils/solace_api.py

+86-12
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
33

44
from __future__ import (absolute_import, division, print_function)
5+
import os
56
__metaclass__ = type
67

78
from ansible_collections.solace.pubsub_plus.plugins.module_utils import solace_sys
89
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_utils import SolaceUtils
910
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_consts import SolaceTaskOps
10-
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_error import SolaceCloudApiResponseDataError, SolaceInternalErrorAbstractMethod, SolaceApiError, SolaceParamsValidationError
11+
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_error import SolaceCloudApiResponseDataError, SolaceEnvVarError, SolaceInternalErrorAbstractMethod, SolaceApiError, SolaceParamsValidationError
1112
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_error import SolaceInternalError
1213
from ansible_collections.solace.pubsub_plus.plugins.module_utils.solace_task_config import SolaceTaskConfig, SolaceTaskBrokerConfig, SolaceTaskSolaceCloudConfig
1314
from ansible.module_utils.basic import AnsibleModule
@@ -489,7 +490,12 @@ def get_objects(self, config: SolaceTaskBrokerConfig, xml_cmd: str, reponse_list
489490

490491
class SolaceCloudApi(SolaceApi):
491492

492-
API_BASE_PATH = "https://api.solace.cloud/api/v0"
493+
ENV_VAR_ANSIBLE_SOLACE_SOLACE_CLOUD_HOME = "ANSIBLE_SOLACE_SOLACE_CLOUD_HOME"
494+
ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US = "us"
495+
ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_AU = "au"
496+
API_BASE_PATH_US = "https://api.solace.cloud/api/v0"
497+
API_BASE_PATH_AU = "https://api.solacecloud.com.au/api/v0"
498+
493499
API_DATA_CENTERS = "datacenters"
494500
API_SERVICES = "services"
495501
API_REQUESTS = "requests"
@@ -498,6 +504,28 @@ def __init__(self, module: AnsibleModule):
498504
super().__init__(module)
499505
return
500506

507+
def get_api_base_path(self, config: SolaceTaskSolaceCloudConfig) -> str:
508+
solace_cloud_home_value = self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US
509+
510+
if config.solace_cloud_home is not None and config.solace_cloud_home != '':
511+
solace_cloud_home_value = config.solace_cloud_home.lower()
512+
else:
513+
solaceCloudHomeEnvVal = os.getenv(
514+
self.ENV_VAR_ANSIBLE_SOLACE_SOLACE_CLOUD_HOME)
515+
if solaceCloudHomeEnvVal is not None and solaceCloudHomeEnvVal != '':
516+
if solaceCloudHomeEnvVal.lower() == self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US:
517+
solace_cloud_home_value = self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US
518+
elif solaceCloudHomeEnvVal.lower() == self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_AU:
519+
solace_cloud_home_value = self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_AU
520+
else:
521+
raise SolaceEnvVarError(self.ENV_VAR_ANSIBLE_SOLACE_SOLACE_CLOUD_HOME,
522+
solaceCloudHomeEnvVal,
523+
f"allowed values: {self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US}, {self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_AU}")
524+
if solace_cloud_home_value == self.ANSIBLE_SOLACE_SOLACE_CLOUD_HOME_US:
525+
return self.API_BASE_PATH_US
526+
else:
527+
return self.API_BASE_PATH_AU
528+
501529
def get_auth(self, config: SolaceTaskBrokerConfig) -> str:
502530
return config.get_solace_cloud_auth()
503531

@@ -524,7 +552,7 @@ def handle_bad_response(self, resp, module_op):
524552
def get_data_centers(self, config: SolaceTaskSolaceCloudConfig) -> list:
525553
# GET /api/v0/datacenters
526554
resp = self.make_get_request(
527-
config, [self.API_BASE_PATH, self.API_DATA_CENTERS], query_params=None)
555+
config, [self.get_api_base_path(config), self.API_DATA_CENTERS], query_params=None)
528556
return resp
529557

530558
def _transform_service(self, service: dict) -> dict:
@@ -546,7 +574,7 @@ def get_services(self, config: SolaceTaskSolaceCloudConfig) -> list:
546574
module_op = SolaceTaskOps.OP_READ_OBJECT_LIST
547575
try:
548576
_resp = self.make_get_request(
549-
config, [self.API_BASE_PATH, self.API_SERVICES], module_op)
577+
config, [self.get_api_base_path(config), self.API_SERVICES], module_op)
550578
except SolaceApiError as e:
551579
resp = e.get_resp()
552580
# TODO: what is the code if solace cloud account has 0 services?
@@ -582,7 +610,7 @@ def get_service(self, config: SolaceTaskSolaceCloudConfig, service_id: str) -> d
582610
module_op = SolaceTaskOps.OP_READ_OBJECT
583611
try:
584612
_resp = self.make_get_request(
585-
config, [self.API_BASE_PATH, self.API_SERVICES, service_id], module_op)
613+
config, [self.get_api_base_path(config), self.API_SERVICES, service_id], module_op)
586614
except SolaceApiError as e:
587615
resp = e.get_resp()
588616
if resp['status_code'] == 404:
@@ -603,7 +631,7 @@ def create_service(self, config: SolaceTaskSolaceCloudConfig, wait_timeout_minut
603631
# POST https://api.solace.cloud/api/v0/services
604632
module_op = SolaceTaskOps.OP_CREATE_OBJECT
605633
resp = self.make_post_request(
606-
config, [self.API_BASE_PATH, self.API_SERVICES], data, module_op)
634+
config, [self.get_api_base_path(config), self.API_SERVICES], data, module_op)
607635
_service_id = resp['serviceId']
608636
if wait_timeout_minutes > 0:
609637
res = self.wait_for_service_create_completion(
@@ -671,7 +699,7 @@ def wait_for_service_create_completion(self, config: SolaceTaskSolaceCloudConfig
671699

672700
def delete_service(self, config: SolaceTaskSolaceCloudConfig, service_id: str) -> dict:
673701
# DELETE https://api.solace.cloud/api/v0/services/{{serviceId}}
674-
path_array = [SolaceCloudApi.API_BASE_PATH,
702+
path_array = [self.get_api_base_path(config),
675703
SolaceCloudApi.API_SERVICES, service_id]
676704
return self.make_delete_request(config, path_array)
677705

@@ -702,7 +730,7 @@ def get_object_settings(self, config: SolaceTaskBrokerConfig, path_array: list)
702730
def get_service_request_status(self, config: SolaceTaskBrokerConfig, service_id: str, request_id: str):
703731
module_op = SolaceTaskOps.OP_READ_OBJECT
704732
# GET https://api.solace.cloud/api/v0/services/{paste-your-serviceId-here}/requests/{{requestId}}
705-
path_array = [self.API_BASE_PATH, self.API_SERVICES,
733+
path_array = [self.get_api_base_path(config), self.API_SERVICES,
706734
service_id, self.API_REQUESTS, request_id]
707735
resp = self.make_get_request(config, path_array, module_op)
708736
# resp may not yet contain 'adminProgress' depending on whether this creation has started yet
@@ -711,16 +739,62 @@ def get_service_request_status(self, config: SolaceTaskBrokerConfig, service_id:
711739
resp['adminProgress'] = 'inProgress'
712740
return resp
713741

742+
def wait_for_service_requests_to_finish(self, config: SolaceTaskBrokerConfig, timeout_minutes: int, service_id: str):
743+
module_op = SolaceTaskOps.OP_READ_OBJECT
744+
# GET https://api.solace.cloud/api/v0/services/{paste-your-serviceId-here}/requests
745+
# returns list of dicts,
746+
# - check all elements,
747+
# - if "adminProgress" == "inProgress", wait and try again
748+
# - raise SolaceApiError if timeout
749+
are_all_completed = False
750+
try_count = -1
751+
delay = 30 # seconds
752+
max_retries = (timeout_minutes * 60) // delay
753+
while not are_all_completed and try_count < max_retries:
754+
path_array = [self.get_api_base_path(config), self.API_SERVICES,
755+
service_id, self.API_REQUESTS]
756+
resp = self.make_get_request(config, path_array, module_op)
757+
# logging.debug(f"wait_for_service_requests_to_finish(): resp=\n{json.dumps(resp, indent=2)}")
758+
# iterate through list to check if any adminProgress == inProgress
759+
# use generator:
760+
matches = (
761+
respElem for respElem in resp if respElem['adminProgress'] == 'inProgress')
762+
notCompletedElement = next(matches, None)
763+
if notCompletedElement is None:
764+
are_all_completed = True
765+
try_count += 1
766+
if not are_all_completed and timeout_minutes > 0:
767+
time.sleep(delay)
768+
769+
if not are_all_completed:
770+
msg = [
771+
"timeout waiting for all outstanding service requests to be completed",
772+
f"timeout(mins)={timeout_minutes}",
773+
"request in progress:",
774+
str(notCompletedElement)]
775+
raise SolaceApiError(
776+
resp, msg, self.get_module()._name, module_op)
777+
return
778+
714779
def make_service_post_request(self, config: SolaceTaskBrokerConfig, path_array: list, service_id: str, json_body, module_op):
780+
781+
timeout_minutes = config.get_timeout() // 60
782+
# set min timeout to 5 mins
783+
timeout_minutes = max(timeout_minutes, 5)
784+
785+
# check if there are any jobs still running against this service and wait until completed
786+
self.wait_for_service_requests_to_finish(
787+
config, timeout_minutes, service_id)
788+
789+
# now make the request
715790
resp = self.make_request(config, requests.post, path_array, json_body)
716791
# import logging, json
717792
# logging.debug(f"resp (make_request) = \n{json.dumps(resp, indent=2)}")
718793
request_id = resp['id']
719-
timeout_minutes = 2
720794
is_completed = False
721795
is_failed = False
722796
try_count = -1
723-
delay = 5 # seconds
797+
delay = 15 # seconds
724798
max_retries = (timeout_minutes * 60) // delay
725799
# wait 1 cycle before start polling
726800
time.sleep(delay)
@@ -741,7 +815,7 @@ def make_service_post_request(self, config: SolaceTaskBrokerConfig, path_array:
741815
resp, resp, self.get_module()._name, module_op)
742816
if not is_completed:
743817
msg = [
744-
f"timeout service post request - not completed, state={resp['adminProgress']}", str(resp)]
818+
f"timeout service post request - not completed, timeout(mins)={timeout_minutes}, state={resp['adminProgress']}", str(resp)]
745819
raise SolaceInternalError(msg)
746820
return resp
747821

@@ -794,7 +868,7 @@ def filter(self, settings: dict, query_params: dict) -> dict:
794868

795869
def get_cert_authority(self, config, service_id, cert_authority_name, query_params):
796870
# GET services/{serviceId}/serviceCertificateAuthorities/{certAuthorityName}
797-
path_array = [SolaceCloudApi.API_BASE_PATH, SolaceCloudApi.API_SERVICES,
871+
path_array = [self.get_api_base_path(config), SolaceCloudApi.API_SERVICES,
798872
service_id, 'serviceCertificateAuthorities', cert_authority_name]
799873
resp = self.get_object_settings(config, path_array)
800874
cert_authority = resp['certificate']

src/ansible_collections/solace/pubsub_plus/plugins/module_utils/solace_error.py

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ def __init__(self, param, value, msg):
7777
super().__init__(f"arg '{param}={value}': {msg}")
7878

7979

80+
class SolaceEnvVarError(Exception):
81+
def __init__(self, name, value, msg):
82+
super().__init__(f"invalid env var: '{name}={value}'. {msg}.")
83+
84+
8085
class SolaceFeatureNotSupportedError(Exception):
8186
def __init__(self, feature):
8287
super().__init__(f"feature: '{feature}")

0 commit comments

Comments
 (0)