Skip to content

Commit

Permalink
Add support for local MSP, channel config
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Elder <celder@Chriss-MacBook-Pro.local>
  • Loading branch information
Chris Elder authored and Chris Elder committed Apr 22, 2024
1 parent b1e3c98 commit c100a7d
Show file tree
Hide file tree
Showing 7 changed files with 791 additions and 4 deletions.
7 changes: 5 additions & 2 deletions plugins/module_utils/msp_utils.py
Expand Up @@ -28,14 +28,17 @@
'''


def convert_identity_to_msp_path(identity):
def convert_identity_to_msp_path(identity, path='temp'):

# Ensure the identity has a CA, otherwise we cannot use it.
if not identity.ca:
raise Exception('The specified identity cannot be used as it does not have a CA field')

# Create a temporary directory.
msp_path = tempfile.mkdtemp()
if path == 'temp':
msp_path = tempfile.mkdtemp()
else:
msp_path = path

# Create the admin certificates directory (ideally would be empty, but
# needs something in it to keep the CLI quiet).
Expand Down
21 changes: 21 additions & 0 deletions plugins/module_utils/utils.py
Expand Up @@ -243,6 +243,27 @@ def get_all_orderering_service_nodes(console):
return ordering_service_nodes


def get_all_organizations(console):

# Go over each peer.
organizations = list()

components = console.get_all_components_by_type('msp')

if len(components) == 0:
return None

for component in components:

data = console.extract_organization_info(component)

# Add the ordering service node.
organizations.append(Organization.from_json(data))

# Return the list of ordering service nodes.
return organizations


def get_ordering_service_by_name(console, name, fail_on_missing=True):

# Look up the ordering service by name.
Expand Down
62 changes: 61 additions & 1 deletion plugins/modules/channel_config.py
Expand Up @@ -17,6 +17,8 @@
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import _load_params, env_fallback

from pathlib import Path

from ..module_utils.dict_utils import diff_dicts
from ..module_utils.fabric_utils import get_fabric_cfg_path
from ..module_utils.file_utils import get_temp_file
Expand Down Expand Up @@ -761,6 +763,60 @@ def sign_update(module):
shutil.rmtree(fabric_cfg_path)


def sign_update_organizations(module):

# Get the channel and target path.
path = module.params['path']

organizations_dir_param = module.params['organizations_dir']

organizations_dir = Path(organizations_dir_param).resolve()

hsm = module.params['hsm']

# Load in the existing config update file and see if we've already signed it.
with open(path, 'rb') as file:
config_update_envelope_json = proto_to_json('common.Envelope', file.read())
signatures = config_update_envelope_json['payload']['data'].get('signatures', list())

for msp_id in module.params['organizations']:

for signature in signatures:
if msp_id == signature['signature_header']['creator']['mspid']:
continue

# Need to sign it.
msp_path = os.path.join(organizations_dir, msp_id, "msp")
fabric_cfg_path = get_fabric_cfg_path()

try:
env = os.environ.copy()
env['CORE_PEER_MSPCONFIGPATH'] = msp_path
env['CORE_PEER_LOCALMSPID'] = msp_id
env['FABRIC_CFG_PATH'] = fabric_cfg_path
module.json_log({
'CORE_PEER_MSPCONFIGPATH': msp_path,
'CORE_PEER_LOCALMSPID': msp_id,
'FABRIC_CFG_PATH': fabric_cfg_path
})
if hsm:
env['CORE_PEER_BCCSP_DEFAULT'] = 'PKCS11'
env['CORE_PEER_BCCSP_PKCS11_LIBRARY'] = hsm['pkcs11library']
env['CORE_PEER_BCCSP_PKCS11_LABEL'] = hsm['label']
env['CORE_PEER_BCCSP_PKCS11_PIN'] = hsm['pin']
env['CORE_PEER_BCCSP_PKCS11_HASH'] = 'SHA2'
env['CORE_PEER_BCCSP_PKCS11_SECURITY'] = '256'
env['CORE_PEER_BCCSP_PKCS11_FILEKEYSTORE_KEYSTORE'] = os.path.join(msp_path, 'keystore')
subprocess.run([
'peer', 'channel', 'signconfigtx', '-f', path
], env=env, text=True, close_fds=True, check=True, capture_output=True)

finally:
shutil.rmtree(fabric_cfg_path)

module.exit_json(changed=True, path=path)


def apply_update(module):

# Log in to the console.
Expand Down Expand Up @@ -801,7 +857,7 @@ def main():
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'),
operation=dict(type='str', required=True, choices=['create', 'fetch', 'compute_update', 'sign_update', 'apply_update']),
operation=dict(type='str', required=True, choices=['create', 'fetch', 'compute_update', 'sign_update', 'sign_update_organizations', 'apply_update']),
ordering_service=dict(type='raw'),
ordering_service_nodes=dict(type='list', elements='raw'),
tls_handshake_time_shift=dict(type='str', fallback=(env_fallback, ['IBP_TLS_HANDSHAKE_TIME_SHIFT'])), # TODO: Look into renaming this env variable
Expand All @@ -817,6 +873,7 @@ def main():
original=dict(type='str'),
updated=dict(type='str'),
organizations=dict(type='list', elements='raw'),
organizations_dir=dict(type='str', default='organizations'),
policies=dict(type='dict'),
acls=dict(type='dict'),
capabilities=dict(type='dict', default=dict(), options=dict(
Expand All @@ -839,6 +896,7 @@ def main():
('operation', 'fetch', ['api_endpoint', 'api_authtype', 'api_key', 'identity', 'msp_id', 'name', 'path']),
('operation', 'compute_update', ['name', 'path', 'original', 'updated']),
('operation', 'sign_update', ['identity', 'msp_id', 'name', 'path']),
('operation', 'sign_update_organizations', ['organizations', 'organizations_dir', 'name', 'path']),
('operation', 'apply_update', ['api_endpoint', 'api_authtype', 'api_key', 'identity', 'msp_id', 'name', 'path'])
]
# Ansible doesn't allow us to say "require one of X and Y only if condition A is true",
Expand Down Expand Up @@ -867,6 +925,8 @@ def main():
compute_update(module)
elif operation == 'sign_update':
sign_update(module)
elif operation == 'sign_update_organizations':
sign_update_organizations(module)
elif operation == 'apply_update':
apply_update(module)
else:
Expand Down
177 changes: 177 additions & 0 deletions plugins/modules/channel_members.py
@@ -0,0 +1,177 @@
#!/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.proto_utils import proto_to_json, json_to_proto
from ..module_utils.dict_utils import copy_dict

from ansible.module_utils._text import to_native

ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}

DOCUMENTATION = '''
---
module: channel_members
short_description: Manage anchor peers for a channel
description:
- Manage anchor peers for the whole channel.
- Migrate anchor peer addresses to the newer open source format
- 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 Fabric operations console.
type: str
required: true
api_authtype:
description:
- C(basic) - Authenticate to 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 Fabric operations console.
type: str
required: true
api_secret:
description:
- The API secret for 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 Fabric operations console.
type: int
default: 60
path:
description:
- Path to current the channel configuration file.
- This file can be fetched by using the M(channel_config) module.
- This file will be updated in place. You will need to keep a copy of the original file for computing the configuration
update.
type: str
required: true
operation:
description:
- C(migrate_addresses_to_os) - Convert the anchor peer addresses in the channel to open source standards
type: str
required: true
notes: []
requirements: []
'''

EXAMPLES = '''
- name: Add the organization to the channel
hyperledger.fabric_ansible_collection.channel_member:
state: present
api_endpoint: https://console.example.org:32000
api_authtype: basic
api_key: xxxxxxxx
api_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
path: updated_config.bin
operation: 'migrate_addresses_to_os'
'''

RETURN = '''
---
{}
'''


def migrate_addresses_to_os(module):

changed = False

# Get the organization and the target path.
path = module.params['path']

# Read the config.
with open(path, 'rb') as file:
config_json = proto_to_json('common.Config', file.read())

original_config_json = copy_dict(config_json)

# check to see if this is a system channel
if 'Consortiums' in config_json['channel_group']['groups']:
module.exit_json(changed=changed, msps=None, original_config_json=None, updated_config_json=None)

# Check to see if the channel member exists.
application_groups = config_json['channel_group']['groups']['Application']['groups']

organizations = list()

for msp_id in application_groups:

msp = config_json['channel_group']['groups']['Application']['groups'][msp_id]

anchor_peers_value = msp['values']['AnchorPeers']['value']['anchor_peers']

for idx, anchor_peer in enumerate(anchor_peers_value):

old_hostname = anchor_peer['host'].split(".")

if not old_hostname[0].endswith('-peer'):
old_hostname[0] = old_hostname[0] + '-peer'
new_host_name = '.'.join(str(hostname_part) for hostname_part in old_hostname)
anchor_peers_value[idx]['host'] = new_host_name
changed = True

if anchor_peer['port'] != 443:
anchor_peers_value[idx]['port'] = 443
changed = True

if changed:
organizations.append(msp_id)

# Save the config.
config_proto = json_to_proto('common.Config', config_json)
with open(path, 'wb') as file:
file.write(config_proto)
module.exit_json(changed=changed, organizations=organizations, original_config_json=original_config_json, updated_config_json=config_json)


def main():

# Create the module.
argument_spec = dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
api_endpoint=dict(type='str', required=True),
api_authtype=dict(type='str', choices=['ibmcloud', 'basic'], required=True),
api_key=dict(type='str', no_log=True, required=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'),
path=dict(type='str', required=True),
operation=dict(type='str', required=True, choices=['migrate_addresses_to_os'])
)
required_if = [
('api_authtype', 'basic', ['api_secret']),
('operation', 'migrate_addresses_to_os', ['api_endpoint', 'api_authtype', 'api_key', 'path'])
]
module = BlockchainModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if)

# Ensure all exceptions are caught.
try:
operation = module.params['operation']
if operation == 'migrate_addresses_to_os':
migrate_addresses_to_os(module)
else:
raise Exception(f'Invalid operation {operation}')

# Notify Ansible of the exception.
except Exception as e:
module.fail_json(msg=to_native(e))


if __name__ == '__main__':
main()

0 comments on commit c100a7d

Please sign in to comment.