From 94ce3cf5227174c3b424866916ae94baf84dfb73 Mon Sep 17 00:00:00 2001 From: Chris Elder Date: Sat, 6 Apr 2024 18:07:00 -0400 Subject: [PATCH] Add metadata update for CA, peer, ordering nodes Signed-off-by: Chris Elder --- plugins/module_utils/consoles.py | 69 ++++++ .../modules/certificate_authority_metadata.py | 207 ++++++++++++++++++ plugins/modules/channel_consenter.py | 34 ++- .../modules/ordering_service_node_metadata.py | 207 ++++++++++++++++++ plugins/modules/peer_metadata.py | 206 +++++++++++++++++ 5 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 plugins/modules/certificate_authority_metadata.py create mode 100644 plugins/modules/ordering_service_node_metadata.py create mode 100644 plugins/modules/peer_metadata.py diff --git a/plugins/module_utils/consoles.py b/plugins/module_utils/consoles.py index ed01d8fd..14fcffe5 100644 --- a/plugins/module_utils/consoles.py +++ b/plugins/module_utils/consoles.py @@ -317,6 +317,29 @@ def delete_ca(self, id): continue return self.handle_error('Failed to delete certificate authority', e) + def update_metadata_ca(self, id, data): + self._ensure_loggedin() + url = urllib.parse.urljoin(self.api_base_url, f'./components/fabric-ca/{id}') + + headers = { + 'Accepts': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': self.authorization + } + data = json.dumps(data) + for attempt in range(1, self.retries + 1): + try: + self.module.json_log({'msg': 'attempting to submit update to certificate authority', 'data': data, 'url': url, 'attempt': attempt, 'api_timeout': self.api_timeout}) + response = open_url(url, data, headers, 'PUT', validate_certs=False, timeout=self.api_timeout, follow_redirects='all') + result = json.load(response) + self.module.json_log({'msg': 'submitted update to certificate authority', 'result': result}) + return result + except Exception as e: + self.module.json_log({'msg': 'failed to submit update to certificate authority', 'error': str(e)}) + if self.should_retry_error(e, attempt): + continue + return self.handle_error('Failed to submit update to certificate authority', e) + def action_ca(self, id, data): self._ensure_loggedin() url = urllib.parse.urljoin(self.api_base_url, f'./kubernetes/components/fabric-ca/{id}/actions') @@ -483,6 +506,29 @@ def _update_peer(self, id, data, ignore_warnings): continue return self.handle_error('Failed to update peer', e) + def update_metadata_peer(self, id, data): + self._ensure_loggedin() + url = urllib.parse.urljoin(self.api_base_url, f'./components/fabric-peer/{id}') + + headers = { + 'Accepts': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': self.authorization + } + data = json.dumps(data) + for attempt in range(1, self.retries + 1): + try: + self.module.json_log({'msg': 'attempting to submit update to peer', 'data': data, 'url': url, 'attempt': attempt, 'api_timeout': self.api_timeout}) + response = open_url(url, data, headers, 'PUT', validate_certs=False, timeout=self.api_timeout, follow_redirects='all') + result = json.load(response) + self.module.json_log({'msg': 'submitted update to peer', 'result': result}) + return result + except Exception as e: + self.module.json_log({'msg': 'failed to submit update to peer', 'error': str(e)}) + if self.should_retry_error(e, attempt): + continue + return self.handle_error('Failed to submit update to peer', e) + def action_peer(self, id, data): self._ensure_loggedin() url = urllib.parse.urljoin(self.api_base_url, f'./kubernetes/components/fabric-peer/{id}/actions') @@ -779,6 +825,29 @@ def delete_ordering_service_node(self, id): continue return self.handle_error('Failed to delete ordering service node', e) + def update_metadata_ordering_service_node(self, id, data): + self._ensure_loggedin() + url = urllib.parse.urljoin(self.api_base_url, f'./components/fabric-orderer/{id}') + + headers = { + 'Accepts': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': self.authorization + } + data = json.dumps(data) + for attempt in range(1, self.retries + 1): + try: + self.module.json_log({'msg': 'attempting to submit update to ordering service node', 'data': data, 'url': url, 'attempt': attempt, 'api_timeout': self.api_timeout}) + response = open_url(url, data, headers, 'PUT', validate_certs=False, timeout=self.api_timeout, follow_redirects='all') + result = json.load(response) + self.module.json_log({'msg': 'submitted update to ordering service node', 'result': result}) + return result + except Exception as e: + self.module.json_log({'msg': 'failed to submit update to ordering service node', 'error': str(e)}) + if self.should_retry_error(e, attempt): + continue + return self.handle_error('Failed to submit update to ordering service node', e) + def action_ordering_service_node(self, id, data): self._ensure_loggedin() url = urllib.parse.urljoin(self.api_base_url, f'./kubernetes/components/fabric-orderer/{id}/actions') diff --git a/plugins/modules/certificate_authority_metadata.py b/plugins/modules/certificate_authority_metadata.py new file mode 100644 index 00000000..ab44cc93 --- /dev/null +++ b/plugins/modules/certificate_authority_metadata.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ..module_utils.module import BlockchainModule +from ..module_utils.certificate_authorities import CertificateAuthority +from ..module_utils.utils import get_console + +from ansible.module_utils._text import to_native + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: certificate_authority_metadata +short_description: Update information about a Hyperledger Fabric certificate authority metadata +description: + - Updates metadata for a certificate authority. + - This module works with the IBM Support for Hyperledger Fabric software or the Hyperledger Fabric + Open Source Stack running in a Red Hat OpenShift or Kubernetes cluster. +author: Chris Elder +options: + api_endpoint: + description: + - The URL for the the Fabric operations console. + type: str + required: true + api_authtype: + description: + - C(basic) - Authenticate to the the Fabric operations console using basic authentication. + You must provide both a valid API key using I(api_key) and API secret using I(api_secret). + type: str + required: true + api_key: + description: + - The API key for the the Fabric operations console. + type: str + required: true + api_secret: + description: + - The API secret for the the Fabric operations console. + - Only required when I(api_authtype) is C(basic). + type: str + api_timeout: + description: + - The timeout, in seconds, to use when interacting with the the Fabric operations console. + type: int + default: 60 + name: + description: + - The name of the certificate authority. + required: true + preferred_url: + description: + - Preferred URL style for the components. + - os is used the open source style with standard ports (443) + - legacy is the software as a service style URLs + required: false + wait_timeout: + description: + - The timeout, in seconds, to wait until the certificate authority is available. + type: int + default: 60 +notes: [] +requirements: [] +''' + +EXAMPLES = ''' +- name: Get certificate authority + hyperledger.fabric_ansible_collection.certificate_authority_action: + api_endpoint: https://console.example.org:32000 + api_authtype: basic + api_key: xxxxxxxx + api_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + name: Org1 CA + preferred_url: os +''' + +RETURN = ''' +--- +exists: + description: + - True if the certificate authority exists, false otherwise. + returned: always + type: boolean +certificate_authority: + description: + - The certificate authority. + returned: if certificate authority exists + type: dict + contains: + name: + description: + - The name of the certificate authority. + type: str + sample: Org1 CA + api_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + operations_url: + description: + - The URL for the operations service of the certificate authority. + type: str + sample: https://org1ca-operations.example.org:32000 + ca_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + ca_name: + description: + - The certificate authority name to use for enrollment requests. + type: str + sample: ca + tlsca_name: + description: + - The certificate authority name to use for TLS enrollment requests. + type: str + sample: tlsca + location: + description: + - The location of the certificate authority. + type: str + sample: ibmcloud + pem: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... + tls_cert: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... +''' + + +def main(): + + # Create the module. + argument_spec = dict( + api_endpoint=dict(type='str', required=True), + api_authtype=dict(type='str', required=True, choices=['ibmcloud', 'basic']), + api_key=dict(type='str', required=True, no_log=True), + api_secret=dict(type='str', no_log=True), + api_timeout=dict(type='int', default=60), + api_token_endpoint=dict(type='str', default='https://iam.cloud.ibm.com/identity/token'), + name=dict(type='str', required=True), + preferred_url=dict(type='str', choices=['os', 'legacy']), + wait_timeout=dict(type='int', default=600) + ) + required_if = [ + ('api_authtype', 'basic', ['api_secret']) + ] + module = BlockchainModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) + + # Ensure all exceptions are caught. + try: + + timeout = module.params['wait_timeout'] + + # Log in to the console. + console = get_console(module) + + changed = False + + # Determine if the certificate authority exists. + certificate_authority = console.get_component_by_display_name('fabric-ca', module.params['name'], deployment_attrs='included') + + # If it doesn't exist, return now. + if certificate_authority is None: + return module.exit_json(exists=False) + + if module.params['preferred_url'] is None: + return module.exit_json(exists=False) + + certificate_authority_metadata_update = dict( + preferred_url=module.params['preferred_url'] + ) + + certificate_authority = console.update_metadata_ca(certificate_authority['id'], certificate_authority_metadata_update) + changed = True + + certificate_authority = CertificateAuthority.from_json(console.extract_ca_info(certificate_authority)) + timeout = module.params['wait_timeout'] + certificate_authority.wait_for(timeout) + + # Return certificate authority information. + module.exit_json(changed=changed, certificate_authority=certificate_authority.to_json()) + + # Notify Ansible of the exception. + except Exception as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/channel_consenter.py b/plugins/modules/channel_consenter.py index 5aebbda6..df1a09f3 100644 --- a/plugins/modules/channel_consenter.py +++ b/plugins/modules/channel_consenter.py @@ -83,6 +83,15 @@ - You can also pass a dict, which must match the result format of one of the M(ordering_service_node_info) or M(ordering_service_node) modules. type: raw + required: true + updated_ordering_service_node: + description: + - The updated ordering service node to use as a consenter for this channel. + - This option must used if the host or port is to be updated in the consenter set. + - This must be passed as a dict, which must match the result format of one + of the M(ordering_service_node_info) or M(ordering_service_node) modules. + type: raw + required: false notes: [] requirements: [] ''' @@ -127,7 +136,8 @@ def main(): api_timeout=dict(type='int', default=60), api_token_endpoint=dict(type='str', default='https://iam.cloud.ibm.com/identity/token'), path=dict(type='str', required=True), - ordering_service_node=dict(type='raw', required=True) + ordering_service_node=dict(type='raw', required=True), + updated_ordering_service_node=dict(type='raw') ) required_if = [ ('api_authtype', 'basic', ['api_secret']) @@ -181,6 +191,26 @@ def main(): break consenter_exists = consenter_idx > -1 + # use the updated_ordering_service_node if the host or port in the consenter is changing + # the existing host and port is needed to determine the consenter to be updated + if module.params['updated_ordering_service_node'] is not None: + + # Get the ordering service node. + ordering_service_node = get_ordering_service_node_by_module(console, module, 'updated_ordering_service_node') + + # Build the expected consenter based on the new ordering node. + parsed_api_url = urllib.parse.urlparse(ordering_service_node.api_url) + host = parsed_api_url.hostname + port = parsed_api_url.port or 443 + client_tls_cert = ordering_service_node.client_tls_cert or ordering_service_node.tls_cert + server_tls_cert = ordering_service_node.server_tls_cert or ordering_service_node.tls_cert + expected_consenter = dict( + host=host, + port=port, + client_tls_cert=client_tls_cert, + server_tls_cert=server_tls_cert, + ) + # Handle the desired state appropriately. state = module.params['state'] if state == 'present' and not consenter_exists: @@ -227,7 +257,7 @@ def main(): config_proto = json_to_proto('common.Config', config_json) with open(path, 'wb') as file: file.write(config_proto) - module.exit_json(changed=True) + module.exit_json(changed=True, original_config_json=original_config_json, updated_config_json=config_json) # Notify Ansible of the exception. except Exception as e: diff --git a/plugins/modules/ordering_service_node_metadata.py b/plugins/modules/ordering_service_node_metadata.py new file mode 100644 index 00000000..28438ac8 --- /dev/null +++ b/plugins/modules/ordering_service_node_metadata.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ..module_utils.module import BlockchainModule +from ..module_utils.ordering_services import OrderingServiceNode +from ..module_utils.utils import get_console + +from ansible.module_utils._text import to_native + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: certificate_authority_metadata +short_description: Update metadate fpr a Hyperledger Fabric certificate authority +description: + - Update metadata for a Hyperledger Fabric certificate authority. + - This module works with the IBM Support for Hyperledger Fabric software or the Hyperledger Fabric + Open Source Stack running in a Red Hat OpenShift or Kubernetes cluster. +author: Chris Elder +options: + api_endpoint: + description: + - The URL for the the Fabric operations console. + type: str + required: true + api_authtype: + description: + - C(basic) - Authenticate to the the Fabric operations console using basic authentication. + You must provide both a valid API key using I(api_key) and API secret using I(api_secret). + type: str + required: true + api_key: + description: + - The API key for the the Fabric operations console. + type: str + required: true + api_secret: + description: + - The API secret for the the Fabric operations console. + - Only required when I(api_authtype) is C(basic). + type: str + api_timeout: + description: + - The timeout, in seconds, to use when interacting with the the Fabric operations console. + type: int + default: 60 + name: + description: + - The name of the certificate authority. + required: true + preferred_url: + description: + - Preferred URL style for the components. + - os is used the open source style with standard ports (443) + - legacy is the software as a service style URLs + wait_timeout: + description: + - The timeout, in seconds, to wait until the certificate authority is available. + type: int + default: 60 +notes: [] +requirements: [] +''' + +EXAMPLES = ''' +- name: Get certificate authority + hyperledger.fabric_ansible_collection.certificate_authority_action: + api_endpoint: https://console.example.org:32000 + api_authtype: basic + api_key: xxxxxxxx + api_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + name: Ordering Service_1 + preferred_url: os + type: tls_cert +''' + +RETURN = ''' +--- +exists: + description: + - True if the certificate authority exists, false otherwise. + returned: always + type: boolean +certificate_authority: + description: + - The certificate authority. + returned: if certificate authority exists + type: dict + contains: + name: + description: + - The name of the certificate authority. + type: str + sample: Org1 CA + api_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + operations_url: + description: + - The URL for the operations service of the certificate authority. + type: str + sample: https://org1ca-operations.example.org:32000 + ca_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + ca_name: + description: + - The certificate authority name to use for enrollment requests. + type: str + sample: ca + tlsca_name: + description: + - The certificate authority name to use for TLS enrollment requests. + type: str + sample: tlsca + location: + description: + - The location of the certificate authority. + type: str + sample: ibmcloud + pem: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... + tls_cert: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... +''' + + +def main(): + + # Create the module. + argument_spec = dict( + api_endpoint=dict(type='str', required=True), + api_authtype=dict(type='str', required=True, choices=['ibmcloud', 'basic']), + api_key=dict(type='str', required=True, no_log=True), + api_secret=dict(type='str', no_log=True), + api_timeout=dict(type='int', default=60), + api_token_endpoint=dict(type='str', default='https://iam.cloud.ibm.com/identity/token'), + name=dict(type='str', required=True), + preferred_url=dict(type='str', choices=['os', 'legacy']), + wait_timeout=dict(type='int', default=600) + ) + required_if = [ + ('api_authtype', 'basic', ['api_secret']) + ] + module = BlockchainModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) + + # Ensure all exceptions are caught. + try: + + timeout = module.params['wait_timeout'] + + # Log in to the console. + console = get_console(module) + + changed = False + + # Determine if the certificate authority exists. + ordering_service_node = console.get_component_by_display_name('fabric-orderer', module.params['name'], deployment_attrs='included') + + # If it doesn't exist, return now. + if ordering_service_node is None: + return module.exit_json(exists=False) + + if module.params['preferred_url'] is None: + return module.exit_json(exists=False) + + ordering_service_node_metadata_update = dict( + preferred_url=module.params['preferred_url'] + ) + + ordering_service_node = console.update_metadata_ordering_service_node(ordering_service_node['id'], ordering_service_node_metadata_update) + changed = True + + ordering_service_node = OrderingServiceNode.from_json(console.extract_ordering_service_node_info(ordering_service_node)) + timeout = module.params['wait_timeout'] + ordering_service_node.wait_for(timeout) + + # Return certificate authority information. + module.exit_json(changed=changed, ordering_service_node=ordering_service_node.to_json()) + + # Notify Ansible of the exception. + except Exception as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/peer_metadata.py b/plugins/modules/peer_metadata.py new file mode 100644 index 00000000..41e23956 --- /dev/null +++ b/plugins/modules/peer_metadata.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ..module_utils.module import BlockchainModule +from ..module_utils.peers import Peer +from ..module_utils.utils import get_console + +from ansible.module_utils._text import to_native + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: certificate_authority_metadata +short_description: Update metadata for a Hyperledger Fabric certificate authority +description: + - Update metadata for a Hyperledger Fabric certificate authority. + - This module works with the IBM Support for Hyperledger Fabric software or the Hyperledger Fabric + Open Source Stack running in a Red Hat OpenShift or Kubernetes cluster. +author: Simon Stone (@sstone1) +options: + api_endpoint: + description: + - The URL for the the Fabric operations console. + type: str + required: true + api_authtype: + description: + - C(basic) - Authenticate to the the Fabric operations console using basic authentication. + You must provide both a valid API key using I(api_key) and API secret using I(api_secret). + type: str + required: true + api_key: + description: + - The API key for the the Fabric operations console. + type: str + required: true + api_secret: + description: + - The API secret for the the Fabric operations console. + - Only required when I(api_authtype) is C(basic). + type: str + api_timeout: + description: + - The timeout, in seconds, to use when interacting with the the Fabric operations console. + type: int + default: 60 + name: + description: + - The name of the peer. + required: true + preferred_url: + description: + - Preferred URL style for the components. + - os is used the open source style with standard ports (443) + - legacy is the software as a service style URLs + wait_timeout: + description: + - The timeout, in seconds, to wait until the certificate authority is available. + type: int + default: 60 +notes: [] +requirements: [] +''' + +EXAMPLES = ''' +- name: Get certificate authority + hyperledger.fabric_ansible_collection.certificate_authority_action: + api_endpoint: https://console.example.org:32000 + api_authtype: basic + api_key: xxxxxxxx + api_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + name: Org1 Peer + preferred_url: os +''' + +RETURN = ''' +--- +exists: + description: + - True if the certificate authority exists, false otherwise. + returned: always + type: boolean +certificate_authority: + description: + - The certificate authority. + returned: if certificate authority exists + type: dict + contains: + name: + description: + - The name of the certificate authority. + type: str + sample: Org1 CA + api_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + operations_url: + description: + - The URL for the operations service of the certificate authority. + type: str + sample: https://org1ca-operations.example.org:32000 + ca_url: + description: + - The URL for the API of the certificate authority. + type: str + sample: https://org1ca-api.example.org:32000 + ca_name: + description: + - The certificate authority name to use for enrollment requests. + type: str + sample: ca + tlsca_name: + description: + - The certificate authority name to use for TLS enrollment requests. + type: str + sample: tlsca + location: + description: + - The location of the certificate authority. + type: str + sample: ibmcloud + pem: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... + tls_cert: + description: + - The TLS certificate chain for the certificate authority. + - The TLS certificate chain is returned as a base64 encoded PEM. + type: str + sample: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t... +''' + + +def main(): + + # Create the module. + argument_spec = dict( + api_endpoint=dict(type='str', required=True), + api_authtype=dict(type='str', required=True, choices=['ibmcloud', 'basic']), + api_key=dict(type='str', required=True, no_log=True), + api_secret=dict(type='str', no_log=True), + api_timeout=dict(type='int', default=60), + api_token_endpoint=dict(type='str', default='https://iam.cloud.ibm.com/identity/token'), + name=dict(type='str', required=True), + preferred_url=dict(type='str', choices=['os', 'legacy']), + wait_timeout=dict(type='int', default=600) + ) + required_if = [ + ('api_authtype', 'basic', ['api_secret']) + ] + module = BlockchainModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) + + # Ensure all exceptions are caught. + try: + + timeout = module.params['wait_timeout'] + + # Log in to the console. + console = get_console(module) + + changed = False + + # Determine if the certificate authority exists. + peer = console.get_component_by_display_name('fabric-peer', module.params['name'], deployment_attrs='included') + + # If it doesn't exist, return now. + if peer is None: + return module.exit_json(exists=False) + + if module.params['preferred_url'] is None: + return module.exit_json(exists=False) + + peer_metadata_update = dict( + preferred_url=module.params['preferred_url'] + ) + + peer = console.update_metadata_ca(peer['id'], peer_metadata_update) + changed = True + + peer = Peer.from_json(console.extract_peer_info(peer)) + timeout = module.params['wait_timeout'] + peer.wait_for(timeout) + + # Return certificate authority information. + module.exit_json(changed=changed, peer=peer.to_json()) + + # Notify Ansible of the exception. + except Exception as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == '__main__': + main()