├── tutorial ├── tutorial-network │ ├── README.md │ ├── lib │ │ └── logic.js │ ├── models │ │ └── org.acme.mynetwork.cto │ ├── package.json │ ├── features │ │ ├── support │ │ │ └── index.js │ │ └── sample.feature │ ├── permissions.acl │ ├── .eslintrc.yml │ ├── connection_template.json │ └── test │ │ └── logic.js ├── tutorial-network.zip └── run_tutorial_template.sh ├── README.md ├── parameters.json ├── deployer.rb ├── deploy.sh ├── deploy.ps1 ├── crypto-config_template.yaml ├── scripts ├── configure.sh └── configure-fabric-azureuser.sh ├── nested ├── loadBalancer.json ├── VMAuth-sshPublicKey.json └── VMExtension.json ├── configtx_template.yaml ├── DeploymentHelper.cs └── template.json /tutorial/tutorial-network/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-network 2 | 3 | Tutorial network 4 | -------------------------------------------------------------------------------- /tutorial/tutorial-network.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-hyperledger-artifacts/master/tutorial/tutorial-network.zip -------------------------------------------------------------------------------- /tutorial/tutorial-network/lib/logic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track the trade of a commodity from one trader to another 3 | * @param {org.acme.mynetwork.Trade} trade - the trade to be processed 4 | * @transaction 5 | */ 6 | async function tradeCommodity(trade) { 7 | trade.commodity.owner = trade.newOwner; 8 | let assetRegistry = await getAssetRegistry('org.acme.mynetwork.Commodity'); 9 | await assetRegistry.update(trade.commodity); 10 | } 11 | -------------------------------------------------------------------------------- /tutorial/tutorial-network/models/org.acme.mynetwork.cto: -------------------------------------------------------------------------------- 1 | /** 2 | * My commodity trading network 3 | */ 4 | namespace org.acme.mynetwork 5 | asset Commodity identified by tradingSymbol { 6 | o String tradingSymbol 7 | o String description 8 | o String mainExchange 9 | o Double quantity 10 | --> Trader owner 11 | } 12 | participant Trader identified by tradeId { 13 | o String tradeId 14 | o String firstName 15 | o String lastName 16 | } 17 | transaction Trade { 18 | --> Commodity commodity 19 | --> Trader newOwner 20 | } -------------------------------------------------------------------------------- /tutorial/tutorial-network/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-network", 3 | "version": "0.0.1", 4 | "description": "Tutorial network", 5 | "scripts": { 6 | "prepublish": "mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/tutorial-network.bna", 7 | "pretest": "npm run lint", 8 | "lint": "eslint .", 9 | "test": "nyc mocha -t 0 test/*.js && cucumber-js" 10 | }, 11 | "author": "Hyperledger Composer", 12 | "email": "NA", 13 | "license": "Apache-2.0", 14 | "devDependencies": { 15 | "composer-admin": "^0.19.0", 16 | "composer-cli": "^0.19.0", 17 | "composer-client": "^0.19.0", 18 | "composer-common": "^0.19.0", 19 | "composer-connector-embedded": "^0.19.0", 20 | "composer-cucumber-steps": "^0.19.0", 21 | "chai": "latest", 22 | "chai-as-promised": "latest", 23 | "cucumber": "^2.2.0", 24 | "eslint": "latest", 25 | "nyc": "latest", 26 | "mkdirp": "latest", 27 | "mocha": "latest" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tutorial/tutorial-network/features/support/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | 17 | const composerSteps = require('composer-cucumber-steps'); 18 | const cucumber = require('cucumber'); 19 | 20 | module.exports = function () { 21 | composerSteps.call(this); 22 | }; 23 | 24 | if (cucumber.defineSupportCode) { 25 | cucumber.defineSupportCode((context) => { 26 | module.exports.call(context); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /tutorial/tutorial-network/permissions.acl: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample access control list. 3 | */ 4 | rule Default { 5 | description: "Allow all participants access to all resources" 6 | participant: "ANY" 7 | operation: ALL 8 | resource: "org.acme.mynetwork.*" 9 | action: ALLOW 10 | } 11 | 12 | rule SystemACL { 13 | description: "System ACL to permit all access" 14 | participant: "org.hyperledger.composer.system.Participant" 15 | operation: ALL 16 | resource: "org.hyperledger.composer.system.**" 17 | action: ALLOW 18 | } 19 | 20 | rule NetworkAdminUser { 21 | description: "Grant business network administrators full access to user resources" 22 | participant: "org.hyperledger.composer.system.NetworkAdmin" 23 | operation: ALL 24 | resource: "**" 25 | action: ALLOW 26 | } 27 | 28 | rule NetworkAdminSystem { 29 | description: "Grant business network administrators full access to system resources" 30 | participant: "org.hyperledger.composer.system.NetworkAdmin" 31 | operation: ALL 32 | resource: "org.hyperledger.composer.system.**" 33 | action: ALLOW 34 | } 35 | -------------------------------------------------------------------------------- /tutorial/tutorial-network/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | mocha: true 4 | extends: 'eslint:recommended' 5 | parserOptions: 6 | ecmaVersion: 8 7 | sourceType: 8 | - script 9 | globals: 10 | getFactory: true 11 | getSerializer: true 12 | getAssetRegistry: true 13 | getParticipantRegistry: true 14 | getCurrentParticipant: true 15 | post: true 16 | emit: true 17 | rules: 18 | indent: 19 | - error 20 | - 4 21 | linebreak-style: 22 | - error 23 | - unix 24 | quotes: 25 | - error 26 | - single 27 | semi: 28 | - error 29 | - always 30 | no-unused-vars: 31 | - 0 32 | - args: none 33 | no-console: off 34 | curly: error 35 | eqeqeq: error 36 | no-throw-literal: error 37 | strict: error 38 | dot-notation: error 39 | no-tabs: error 40 | no-trailing-spaces: error 41 | no-useless-call: error 42 | no-with: error 43 | operator-linebreak: error 44 | require-jsdoc: 45 | - error 46 | - require: 47 | ClassDeclaration: true 48 | MethodDefinition: true 49 | FunctionDeclaration: true 50 | yoda: error 51 | no-confusing-arrow: 2 52 | no-constant-condition: 2 53 | -------------------------------------------------------------------------------- /tutorial/run_tutorial_template.sh: -------------------------------------------------------------------------------- 1 | # Following instructions from: 2 | # https://hyperledger.github.io/composer/latest/tutorials/developer-tutorial.html 3 | 4 | 5 | # For debugging create $HOME/tutorial-network/config/default.json 6 | # and put the following in it before running the composer commands: 7 | # { 8 | # "composer": { 9 | # "log": { 10 | # "debug": "composer[debug]:*", 11 | # "console": { 12 | # "maxLevel": "debug" 13 | # }, 14 | # "file": { 15 | # "filename" : "./log.txt" 16 | # } 17 | # } 18 | # } 19 | # } 20 | 21 | cd ~/tutorial-network 22 | 23 | composer archive create -t dir -n . 24 | 25 | cp ~/crypto-config/peerOrganizations/{{PEER_ORG_DOMAIN}}/users/Admin@{{PEER_ORG_DOMAIN}}/msp/signcerts/Admin@{{PEER_ORG_DOMAIN}}-cert.pem . 26 | cp ~/crypto-config/peerOrganizations/{{PEER_ORG_DOMAIN}}/users/Admin@{{PEER_ORG_DOMAIN}}/msp/keystore/* Admin@{{PEER_ORG_DOMAIN}}_sk 27 | 28 | composer card create -p connection.json -u PeerAdmin -c Admin@{{PEER_ORG_DOMAIN}}-cert.pem -k Admin@{{PEER_ORG_DOMAIN}}_sk -r PeerAdmin -r ChannelAdmin 29 | composer card import -f PeerAdmin@tutorial-network.card 30 | composer network install -c PeerAdmin@tutorial-network -a tutorial-network@0.0.1.bna 31 | 32 | composer network start --networkName tutorial-network --networkVersion 0.0.1 -A {{CA_USER}} -S {{CA_PASSWORD}} -c PeerAdmin@tutorial-network 33 | composer card import -f admin@tutorial-network.card 34 | composer network ping -c admin@tutorial-network 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperledger Fabric 1.1.0 and Composer 0.19 on Azure 2 | 3 | This set of templates and scripts is based on the Microsoft [Microsoft Hyperledger Fabric on Azure solution template](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/microsoft-azure-blockchain.azure-blockchain-hyperledger-fabric). It performs a deployment of Hyperledger Fabric 1.1.0 nodes and Hyperledger Composer 0.19.0. 4 | 5 | After deployment you will have: 6 | 7 | - 4 Ubuntu virtual machines, each running a docker container with a specific Hyperledger node: ca0, ordered0, peer0 and peer1. 8 | - peer0 and peer1 will be part of the same channel, called composerchannel. 9 | - Composer is installed on ca0 and there is a shell script `run_tutorial.sh` ready to be called in the root folder to reproduce the steps of the [Deploying a Hyperledger Composer blockchain business network to Hyperledger Fabric for a single organization](https://hyperledger.github.io/composer/latest/tutorials/deploy-to-fabric-single-org) tutorial. 10 | - A business network called `tutorial-network` is deployed. It is the same as the one provided in [Developer tutorial for creating a Hyperledger Composer solution](https://hyperledger.github.io/composer/latest/tutorials/developer-tutorial). 11 | 12 | ## Usage 13 | 14 | 1. Download `template.json`, `parameters.json` and one of the Azure deployment scripts such as `deploy.sh` and save them in the same directory. 15 | 2. Edit the `parameters.json` file and change the value of `adminSSHKey` to the public key of the SSH key you use in your Azure Cloud Shell. Apply other customisation. 16 | 3. From a shell, got to the directory where you saved the file (sep 1) and execute the deployment script. 17 | 18 | The deployment takes approximately 30 minutes. -------------------------------------------------------------------------------- /tutorial/tutorial-network/connection_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-network", 3 | "x-type": "hlfv1", 4 | "version": "1.0.0", 5 | "peers": { 6 | "{{PEER_PREFIX}}0.{{PEER_ORG_DOMAIN}}": { 7 | "url": "grpc://{{PEER_PREFIX}}0:7051", 8 | "eventUrl": "grpc://{{PEER_PREFIX}}0:7053" 9 | }, 10 | "{{PEER_PREFIX}}1.{{PEER_ORG_DOMAIN}}": { 11 | "url": "grpc://{{PEER_PREFIX}}1:7051", 12 | "eventUrl": "grpc://{{PEER_PREFIX}}1:7053" 13 | } 14 | }, 15 | "certificateAuthorities": { 16 | "{{CA_PREFIX}}0.{{PEER_ORG_DOMAIN}}": { 17 | "url": "http://{{CA_PREFIX}}0:7054", 18 | "caName": "{{CA_PREFIX}}0.{{PEER_ORG_DOMAIN}}" 19 | } 20 | }, 21 | "orderers": { 22 | "{{ORDERER_PREFIX}}0.{{ORDERER_ORG_DOMAIN}}": { 23 | "url": "grpc://{{ORDERER_PREFIX}}0:7050" 24 | } 25 | }, 26 | "organizations": { 27 | "Org1": { 28 | "mspid": "Org1MSP", 29 | "peers": [ 30 | "{{PEER_PREFIX}}0.{{PEER_ORG_DOMAIN}}" 31 | ], 32 | "certificateAuthorities": [ 33 | "{{CA_PREFIX}}0.{{PEER_ORG_DOMAIN}}" 34 | ] 35 | } 36 | }, 37 | "channels": { 38 | "composerchannel": { 39 | "orderers": [ 40 | "{{ORDERER_PREFIX}}0.{{ORDERER_ORG_DOMAIN}}" 41 | ], 42 | "peers": { 43 | "{{PEER_PREFIX}}0.{{PEER_ORG_DOMAIN}}": {} 44 | } 45 | } 46 | }, 47 | "client": { 48 | "organization": "Org1", 49 | "connection": { 50 | "timeout": { 51 | "peer": { 52 | "endorser": "300", 53 | "eventHub": "300", 54 | "eventReg": "300" 55 | }, 56 | "orderer": "300" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "namePrefix": { 9 | "value": "mypref" 10 | }, 11 | "authType": { 12 | "value": "sshPublicKey" 13 | }, 14 | "adminUsername": { 15 | "value": "azureuser" 16 | }, 17 | "adminSSHKey": { 18 | "value": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDQHC7Q1pK9uO4IGREoYmIH60j+I+C3ZTqf9pFvKEq8R3Oky5mPY3HZQ6Lno/VTrxL7YqHqpt6DSdaCKy/CUlnaf3agtUItpNj6Ro7lnGUoADUOWRuhJdgN1VmgZxGxrhB1j0b0WQK9PwOL9C2yBuwP6O5VeZU3FUDNODUe7g5VgpmWLNTINB9unr54cUVYXt19c+WA17iHQiWUfPHWCEdQz8wNblbtMRkZdEuWf19xy2QjkZpoS/cRYfy8E7WSYN45P0TigjkX1t/kkdblFOKyBU+1ONOm2/PTVx/yksf/hZtqEUz+B9ovEOdTslyf+12OxdFYucoTvwb31/00UoqT4+3rwZnzkQPA4iIN5rQqr9da52bbz5Hp/5c+fIpfM/0OyucoZ4gyGDNDutuNsFgjo3bRN++QiXlD/kQ+38vJERvMHRVYaDTQ6b925PMfyySE69mqHG/jF/9MYIWF5E2pJjFR5RjMY9lCEyIRlk8rs6pBiG94bDTeDrow90Wm6jijzKbHogJXRU5weHmz3Z+65hDBdPhcUVcwJR//z+vkICcZSldU5Qn9XUDwzPVPJ1hReFZDYdepCinNK5hbUii79KOM+TjzCMGzcwtMYmay0hAUStZJqDTHpEYC+6RagHlrmN2AumsyWQgnoUz8e64msaCdCwIYqtSy2JmyFJBl2Q==" 19 | }, 20 | "restrictAccess": { 21 | "value": 0 22 | }, 23 | "numMembershipNodes": { 24 | "value": 1 25 | }, 26 | "mnNodeVMSize": { 27 | "value": "Standard_A1" 28 | }, 29 | "mnStorageAccountType": { 30 | "value": "Standard_LRS" 31 | }, 32 | "numOrdererNodes": { 33 | "value": 1 34 | }, 35 | "onNodeVMSize": { 36 | "value": "Standard_A1" 37 | }, 38 | "onStorageAccountType": { 39 | "value": "Standard_LRS" 40 | }, 41 | "numPeerNodes": { 42 | "value": 2 43 | }, 44 | "pnNodeVMSize": { 45 | "value": "Standard_A1" 46 | }, 47 | "pnStorageAccountType": { 48 | "value": "Standard_LRS" 49 | }, 50 | "CAUsername": { 51 | "value": "admin" 52 | }, 53 | "CAPassword": { 54 | "value": "MCu6UOiXDa8D" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /deployer.rb: -------------------------------------------------------------------------------- 1 | require 'azure_mgmt_resources' 2 | 3 | class Deployer 4 | 5 | # Initialize the deployer class with subscription, resource group and resource group location. The class will raise an 6 | # ArgumentError if there are empty values for Tenant Id, Client Id or Client Secret environment variables. 7 | # 8 | # @param [String] subscription_id the subscription to deploy the template 9 | # @param [String] resource_group the resource group to create or update and then deploy the template 10 | # @param [String] resource_group_location the location of the resource group 11 | def initialize(subscription_id, resource_group, resource_group_location) 12 | raise ArgumentError.new("Missing template file 'template.json' in current directory.") unless File.exist?('template.json') 13 | raise ArgumentError.new("Missing parameters file 'parameters.json' in current directory.") unless File.exist?('parameters.json') 14 | @resource_group = resource_group 15 | @subscription_id = subscription_id 16 | @resource_group_location = resource_group_location 17 | provider = MsRestAzure::ApplicationTokenProvider.new( 18 | ENV['AZURE_TENANT_ID'], 19 | ENV['AZURE_CLIENT_ID'], 20 | ENV['AZURE_CLIENT_SECRET']) 21 | credentials = MsRest::TokenCredentials.new(provider) 22 | @client = Azure::ARM::Resources::ResourceManagementClient.new(credentials) 23 | @client.subscription_id = @subscription_id 24 | end 25 | 26 | # Deploy the template to a resource group 27 | def deploy 28 | # ensure the resource group is created 29 | params = Azure::ARM::Resources::Models::ResourceGroup.new.tap do |rg| 30 | rg.location = @resource_group_location 31 | end 32 | @client.resource_groups.create_or_update(@resource_group, params).value! 33 | 34 | # build the deployment from a json file template from parameters 35 | template = File.read(File.expand_path(File.join(__dir__, 'template.json'))) 36 | deployment = Azure::ARM::Resources::Models::Deployment.new 37 | deployment.properties = Azure::ARM::Resources::Models::DeploymentProperties.new 38 | deployment.properties.template = JSON.parse(template) 39 | deployment.properties.mode = Azure::ARM::Resources::Models::DeploymentMode::Incremental 40 | 41 | # build the deployment template parameters from Hash to {key: {value: value}} format 42 | deploy_params = File.read(File.expand_path(File.join(__dir__, 'parameters.json'))) 43 | deployment.properties.parameters = JSON.parse(deploy_params)["parameters"] 44 | 45 | # put the deployment to the resource group 46 | @client.deployments.create_or_update(@resource_group, 'azure-sample', deployment) 47 | end 48 | end 49 | 50 | # Get user inputs and execute the script 51 | if(ARGV.empty?) 52 | puts "Please specify subscriptionId resourceGroupName resourceGroupLocation as command line arguments" 53 | exit 54 | end 55 | 56 | subscription_id = ARGV[0] # Azure Subscription Id 57 | resource_group = ARGV[1] # The resource group for deployment 58 | resource_group_location = ARGV[2] # The resource group location 59 | 60 | msg = "\nInitializing the Deployer class with subscription id: #{subscription_id}, resource group: #{resource_group}" 61 | msg += "\nand resource group location: #{resource_group_location}...\n\n" 62 | puts msg 63 | 64 | # Initialize the deployer class 65 | deployer = Deployer.new(subscription_id, resource_group, resource_group_location) 66 | 67 | puts "Beginning the deployment... \n\n" 68 | # Deploy the template 69 | deployment = deployer.deploy 70 | 71 | puts "Done deploying!!" -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # -e: immediately exit if any command has a non-zero exit status 6 | # -o: prevents errors in a pipeline from being masked 7 | # IFS new value is less likely to cause confusing bugs when looping arrays or arguments (e.g. $@) 8 | 9 | usage() { echo "Usage: $0 -i -g -n -l " 1>&2; exit 1; } 10 | 11 | declare subscriptionId="" 12 | declare resourceGroupName="" 13 | declare deploymentName="" 14 | declare resourceGroupLocation="" 15 | 16 | # Initialize parameters specified from command line 17 | while getopts ":i:g:n:l:" arg; do 18 | case "${arg}" in 19 | i) 20 | subscriptionId=${OPTARG} 21 | ;; 22 | g) 23 | resourceGroupName=${OPTARG} 24 | ;; 25 | n) 26 | deploymentName=${OPTARG} 27 | ;; 28 | l) 29 | resourceGroupLocation=${OPTARG} 30 | ;; 31 | esac 32 | done 33 | shift $((OPTIND-1)) 34 | 35 | #Prompt for parameters is some required parameters are missing 36 | if [[ -z "$subscriptionId" ]]; then 37 | echo "Your subscription ID can be looked up with the CLI using: az account show --out json " 38 | echo "Enter your subscription ID:" 39 | read subscriptionId 40 | [[ "${subscriptionId:?}" ]] 41 | fi 42 | 43 | if [[ -z "$resourceGroupName" ]]; then 44 | echo "This script will look for an existing resource group, otherwise a new one will be created " 45 | echo "You can create new resource groups with the CLI using: az group create " 46 | echo "Enter a resource group name" 47 | read resourceGroupName 48 | [[ "${resourceGroupName:?}" ]] 49 | fi 50 | 51 | if [[ -z "$deploymentName" ]]; then 52 | echo "Enter a name for this deployment:" 53 | read deploymentName 54 | fi 55 | 56 | if [[ -z "$resourceGroupLocation" ]]; then 57 | echo "If creating a *new* resource group, you need to set a location " 58 | echo "You can lookup locations with the CLI using: az account list-locations " 59 | 60 | echo "Enter resource group location:" 61 | read resourceGroupLocation 62 | fi 63 | 64 | #templateFile Path - template file to be used 65 | templateFilePath="template.json" 66 | 67 | if [ ! -f "$templateFilePath" ]; then 68 | echo "$templateFilePath not found" 69 | exit 1 70 | fi 71 | 72 | #parameter file path 73 | parametersFilePath="parameters.json" 74 | 75 | if [ ! -f "$parametersFilePath" ]; then 76 | echo "$parametersFilePath not found" 77 | exit 1 78 | fi 79 | 80 | if [ -z "$subscriptionId" ] || [ -z "$resourceGroupName" ] || [ -z "$deploymentName" ]; then 81 | echo "Either one of subscriptionId, resourceGroupName, deploymentName is empty" 82 | usage 83 | fi 84 | 85 | #login to azure using your credentials 86 | az account show 1> /dev/null 87 | 88 | if [ $? != 0 ]; 89 | then 90 | az login 91 | fi 92 | 93 | #set the default subscription id 94 | az account set --subscription $subscriptionId 95 | 96 | set +e 97 | 98 | #Check for existing RG 99 | az group show $resourceGroupName 1> /dev/null 100 | 101 | if [ $? != 0 ]; then 102 | echo "Resource group with name" $resourceGroupName "could not be found. Creating new resource group.." 103 | set -e 104 | ( 105 | set -x 106 | az group create --name $resourceGroupName --location $resourceGroupLocation 1> /dev/null 107 | ) 108 | else 109 | echo "Using existing resource group..." 110 | fi 111 | 112 | #Start deployment 113 | echo "Starting deployment..." 114 | ( 115 | set -x 116 | az group deployment create --name "$deploymentName" --resource-group "$resourceGroupName" --template-file "$templateFilePath" --parameters "@${parametersFilePath}" 117 | ) 118 | 119 | if [ $? == 0 ]; 120 | then 121 | echo "Template has been successfully deployed" 122 | fi 123 | -------------------------------------------------------------------------------- /deploy.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deploys a template to Azure 4 | 5 | .DESCRIPTION 6 | Deploys an Azure Resource Manager template 7 | 8 | .PARAMETER subscriptionId 9 | The subscription id where the template will be deployed. 10 | 11 | .PARAMETER resourceGroupName 12 | The resource group where the template will be deployed. Can be the name of an existing or a new resource group. 13 | 14 | .PARAMETER resourceGroupLocation 15 | Optional, a resource group location. If specified, will try to create a new resource group in this location. If not specified, assumes resource group is existing. 16 | 17 | .PARAMETER deploymentName 18 | The deployment name. 19 | 20 | .PARAMETER templateFilePath 21 | Optional, path to the template file. Defaults to template.json. 22 | 23 | .PARAMETER parametersFilePath 24 | Optional, path to the parameters file. Defaults to parameters.json. If file is not found, will prompt for parameter values based on template. 25 | #> 26 | 27 | param( 28 | [Parameter(Mandatory=$True)] 29 | [string] 30 | $subscriptionId, 31 | 32 | [Parameter(Mandatory=$True)] 33 | [string] 34 | $resourceGroupName, 35 | 36 | [string] 37 | $resourceGroupLocation, 38 | 39 | [Parameter(Mandatory=$True)] 40 | [string] 41 | $deploymentName, 42 | 43 | [string] 44 | $templateFilePath = "template.json", 45 | 46 | [string] 47 | $parametersFilePath = "parameters.json" 48 | ) 49 | 50 | <# 51 | .SYNOPSIS 52 | Registers RPs 53 | #> 54 | Function RegisterRP { 55 | Param( 56 | [string]$ResourceProviderNamespace 57 | ) 58 | 59 | Write-Host "Registering resource provider '$ResourceProviderNamespace'"; 60 | Register-AzureRmResourceProvider -ProviderNamespace $ResourceProviderNamespace; 61 | } 62 | 63 | #****************************************************************************** 64 | # Script body 65 | # Execution begins here 66 | #****************************************************************************** 67 | $ErrorActionPreference = "Stop" 68 | 69 | # sign in 70 | Write-Host "Logging in..."; 71 | Login-AzureRmAccount; 72 | 73 | # select subscription 74 | Write-Host "Selecting subscription '$subscriptionId'"; 75 | Select-AzureRmSubscription -SubscriptionID $subscriptionId; 76 | 77 | # Register RPs 78 | $resourceProviders = @("microsoft.compute","microsoft.network","microsoft.resources"); 79 | if($resourceProviders.length) { 80 | Write-Host "Registering resource providers" 81 | foreach($resourceProvider in $resourceProviders) { 82 | RegisterRP($resourceProvider); 83 | } 84 | } 85 | 86 | #Create or check for existing resource group 87 | $resourceGroup = Get-AzureRmResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue 88 | if(!$resourceGroup) 89 | { 90 | Write-Host "Resource group '$resourceGroupName' does not exist. To create a new resource group, please enter a location."; 91 | if(!$resourceGroupLocation) { 92 | $resourceGroupLocation = Read-Host "resourceGroupLocation"; 93 | } 94 | Write-Host "Creating resource group '$resourceGroupName' in location '$resourceGroupLocation'"; 95 | New-AzureRmResourceGroup -Name $resourceGroupName -Location $resourceGroupLocation 96 | } 97 | else{ 98 | Write-Host "Using existing resource group '$resourceGroupName'"; 99 | } 100 | 101 | # Start the deployment 102 | Write-Host "Starting deployment..."; 103 | if(Test-Path $parametersFilePath) { 104 | New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile $templateFilePath -TemplateParameterFile $parametersFilePath; 105 | } else { 106 | New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile $templateFilePath; 107 | } -------------------------------------------------------------------------------- /crypto-config_template.yaml: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------------- 2 | # "OrdererOrgs" - Definition of organizations managing orderer nodes 3 | # --------------------------------------------------------------------------- 4 | OrdererOrgs: 5 | # --------------------------------------------------------------------------- 6 | # Orderer 7 | # --------------------------------------------------------------------------- 8 | - Name: Orderer 9 | Domain: fabienpe.com 10 | # --------------------------------------------------------------------------- 11 | # "Specs" - See PeerOrgs below for complete description 12 | # --------------------------------------------------------------------------- 13 | Specs: 14 | - Hostname: {{PREFIX}}-orderer0 15 | # --------------------------------------------------------------------------- 16 | # "PeerOrgs" - Definition of organizations managing peer nodes 17 | # --------------------------------------------------------------------------- 18 | PeerOrgs: 19 | # --------------------------------------------------------------------------- 20 | # Org1 21 | # --------------------------------------------------------------------------- 22 | - Name: Org1 23 | Domain: org1.fabienpe.com 24 | # --------------------------------------------------------------------------- 25 | # "Specs" 26 | # --------------------------------------------------------------------------- 27 | # Uncomment this section to enable the explicit definition of hosts in your 28 | # configuration. Most users will want to use Template, below 29 | # 30 | # Specs is an array of Spec entries. Each Spec entry consists of two fields: 31 | # - Hostname: (Required) The desired hostname, sans the domain. 32 | # - CommonName: (Optional) Specifies the template or explicit override for 33 | # the CN. By default, this is the template: 34 | # 35 | # "{{.Hostname}}.{{.Domain}}" 36 | # 37 | # which obtains its values from the Spec.Hostname and 38 | # Org.Domain, respectively. 39 | # --------------------------------------------------------------------------- 40 | # Specs: 41 | # - Hostname: foo # implicitly "foo.org1.fabienpe.com" 42 | # CommonName: foo27.org5.fabienpe.com # overrides Hostname-based FQDN set above 43 | # - Hostname: bar 44 | # - Hostname: baz 45 | # --------------------------------------------------------------------------- 46 | # "Template" 47 | # --------------------------------------------------------------------------- 48 | # Allows for the definition of 1 or more hosts that are created sequentially 49 | # from a template. By default, this looks like "peer%d" from 0 to Count-1. 50 | # You may override the number of nodes (Count), the starting index (Start) 51 | # or the template used to construct the name (Hostname). 52 | # 53 | # Note: Template and Specs are not mutually exclusive. You may define both 54 | # sections and the aggregate nodes will be created for you. Take care with 55 | # name collisions 56 | # --------------------------------------------------------------------------- 57 | Template: 58 | Count: {{PEER_NUM}} 59 | Hostname: {{PREFIX}}-peer{{.Index}} 60 | # Start: 5 61 | # Hostname: {{.Prefix}}{{.Index}} # default 62 | # --------------------------------------------------------------------------- 63 | # "Users" 64 | # --------------------------------------------------------------------------- 65 | # Count: The number of user accounts _in addition_ to Admin 66 | # --------------------------------------------------------------------------- 67 | Users: 68 | Count: 1 69 | -------------------------------------------------------------------------------- /scripts/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Utility function to exit with message 4 | unsuccessful_exit() 5 | { 6 | echo "FATAL: Exiting script due to: $1. Exit code: $2"; 7 | exit $2; 8 | } 9 | 10 | ############# 11 | # Parameters 12 | ############# 13 | # Validate that all arguments are supplied 14 | if [ $# -lt 13 ]; then unsuccessful_exit "Insufficient parameters supplied. Exiting" 200; fi 15 | 16 | NODE_TYPE=$1 17 | AZUREUSER=$2 18 | ARTIFACTS_URL_PREFIX=$3 19 | NODE_INDEX=$4 20 | CA_PREFIX=$5 21 | CA_NUM=$6 22 | ORDERER_PREFIX=$7 23 | ORDERER_NUM=$8 24 | PEER_PREFIX=$9 25 | PEER_NUM=${10} 26 | CA_USER=${11} 27 | CA_PASSWORD=${12} 28 | PREFIX=${13} 29 | 30 | ########### 31 | # Constants 32 | ########### 33 | HOMEDIR="/home/$AZUREUSER"; 34 | CONFIG_LOG_FILE_PATH="$HOMEDIR/config.log"; 35 | 36 | ########################################### 37 | # System packages to be installed as root # 38 | ########################################### 39 | 40 | # Docker installation: https://docs.docker.com/engine/installation/linux/ubuntu/#install-using-the-repository 41 | 42 | # Install packages to allow apt to use a repository over HTTPS: 43 | apt-get -y install apt-transport-https ca-certificates curl software-properties-common 44 | 45 | # Add Docker’s official GPG key: 46 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - 47 | 48 | # Set up the stable repository: 49 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 50 | 51 | # Update the apt package index: 52 | apt-get update 53 | 54 | # Install the latest version of Docker: 55 | apt-get -y install docker-ce 56 | 57 | # Create the docker group: 58 | groupadd docker 59 | 60 | # Add the Azure user to the docker group: 61 | usermod -aG docker $AZUREUSER 62 | 63 | case "${NODE_TYPE}" in 64 | "ca") 65 | # Update package lists 66 | apt-add-repository -y ppa:git-core/ppa 67 | apt-get update 68 | 69 | # Install Unzip 70 | apt-get install unzip 71 | 72 | # Install Git 73 | apt-get install -y git 74 | 75 | # Install nvm dependencies 76 | apt-get -y install build-essential libssl-dev 77 | 78 | # Install python v2 if required 79 | set +e 80 | COUNT="$(python -V 2>&1 | grep -c 2.)" 81 | if [ ${COUNT} -ne 1 ] 82 | then 83 | apt-get install -y python-minimal 84 | fi 85 | ;; 86 | esac 87 | 88 | 89 | ################################################ 90 | # System configuration to be performed as root # 91 | ################################################ 92 | 93 | curl -sL https://deb.nodesource.com/setup_8.x | bash 94 | apt-get -y install nodejs build-essential 95 | npm install gulp -g 96 | 97 | ############# 98 | # Get the script for running as Azure user 99 | ############# 100 | cd "/home/$AZUREUSER"; 101 | 102 | sudo -u $AZUREUSER /bin/bash -c "wget -N ${ARTIFACTS_URL_PREFIX}/scripts/configure-fabric-azureuser.sh"; 103 | 104 | ################################## 105 | # Initiate loop for error checking 106 | ################################## 107 | FAILED_EXITCODE=0 108 | for LOOPCOUNT in `seq 1 5`; do 109 | sudo -u $AZUREUSER /bin/bash /home/$AZUREUSER/configure-fabric-azureuser.sh "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" "${12}" "${13}" "${14}" "${15}" "${16}" "${17}" "${18}" >> $CONFIG_LOG_FILE_PATH 2>&1; 110 | 111 | FAILED_EXITCODE=$? 112 | if [ $FAILED_EXITCODE -ne 0 ]; then 113 | echo "FAILED_EXITCODE: $FAILED_EXITCODE " >> $CONFIG_LOG_FILE_PATH; 114 | echo "Command failed on try $LOOPCOUNT, retrying..." >> $CONFIG_LOG_FILE_PATH; 115 | sleep 5; 116 | continue; 117 | else 118 | echo "======== Deployment successful! ======== " >> $CONFIG_LOG_FILE_PATH; 119 | exit 0; 120 | fi 121 | done 122 | 123 | echo "One or more commands failed after 5 tries. Deployment failed." >> $CONFIG_LOG_FILE_PATH; 124 | unsuccessful_exit "One or more commands failed after 5 tries. Deployment failed." $FAILED_EXITCODE 125 | -------------------------------------------------------------------------------- /nested/loadBalancer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "loadBalancerName": { 6 | "type": "string" 7 | }, 8 | "dnsHostName": { 9 | "type": "string" 10 | }, 11 | "loadBalancerBackendAddressPoolName": { 12 | "type": "string" 13 | }, 14 | "loadBalancerInboundNatRuleNamePrefix": { 15 | "type": "string" 16 | }, 17 | "frontendPort1": { 18 | "type": "int" 19 | }, 20 | "backendPort1": { 21 | "type": "int" 22 | }, 23 | "numInboundNATRules": { 24 | "type": "int" 25 | }, 26 | "inboundNATRuleSSHStartingPort": { 27 | "type": "int" 28 | }, 29 | "inboundNATRuleSSHBackendPort": { 30 | "type": "int" 31 | }, 32 | "publicIPAddressName": { 33 | "type": "string" 34 | }, 35 | "location": { 36 | "type": "string" 37 | } 38 | }, 39 | "variables": { 40 | "apiVersionPublicIPAddresses": "2016-09-01", 41 | "apiVersionLoadBalancers": "2016-09-01", 42 | "apiVersionLoadBalanceInboundNATRules": "2016-09-01", 43 | "lbID": "[resourceId('Microsoft.Network/loadBalancers', parameters('loadBalancerName'))]", 44 | "lbFrontEndIpConfigName": "LoadBalancerFrontEnd", 45 | "lbFrontEndIPConfigID": "[concat(variables('lbID'),'/frontendIPConfigurations/',variables('lbFrontEndIpConfigName'))]", 46 | "lbBackendAddressPoolID": "[concat(variables('lbID'), '/backendAddressPools/', parameters('loadBalancerBackendAddressPoolName'))]", 47 | "loadBalancerInboundNatRuleIDprefix": "[concat(variables('lbID'),'/inboundNatRules/',parameters('loadBalancerInboundNatRuleNamePrefix'))]" 48 | }, 49 | "resources": [ 50 | { 51 | "apiVersion": "[variables('apiVersionLoadBalancers')]", 52 | "name": "[parameters('loadBalancerName')]", 53 | "type": "Microsoft.Network/loadBalancers", 54 | "location": "[parameters('location')]", 55 | "properties": { 56 | "frontendIPConfigurations": [ 57 | { 58 | "name": "[variables('lbFrontEndIpConfigName')]", 59 | "properties": { 60 | "publicIPAddress": { 61 | "id": "[resourceId('Microsoft.Network/publicIPAddresses/', parameters('publicIPAddressName'))]" 62 | } 63 | } 64 | } 65 | ], 66 | "backendAddressPools": [ 67 | { 68 | "name": "[parameters('loadBalancerBackendAddressPoolName')]" 69 | } 70 | ], 71 | "loadBalancingRules": [ 72 | { 73 | "name": "LB-Rule1", 74 | "properties": { 75 | "frontendIPConfiguration": { 76 | "id": "[variables('lbFrontEndIPConfigID')]" 77 | }, 78 | "backendAddressPool": { 79 | "id": "[variables('lbBackendAddressPoolID')]" 80 | }, 81 | "protocol": "Tcp", 82 | "frontendPort": "[parameters('FrontendPort1')]", 83 | "backendPort": "[parameters('BackendPort1')]", 84 | "enableFloatingIP": false, 85 | "idleTimeoutInMinutes": 5, 86 | "probe": { 87 | "id": "[concat(variables('lbID'),'/probes/lbProbe1')]" 88 | } 89 | } 90 | } 91 | ], 92 | "probes": [ 93 | { 94 | "name": "lbProbe1", 95 | "properties": { 96 | "protocol": "Tcp", 97 | "port": "[parameters('BackendPort1')]", 98 | "intervalInSeconds": 5, 99 | "numberOfProbes": 2 100 | } 101 | } 102 | ] 103 | } 104 | }, 105 | { 106 | "apiVersion": "[variables('apiVersionLoadBalanceInboundNATRules')]", 107 | "type": "Microsoft.Network/loadBalancers/inboundNatRules", 108 | "name": "[concat(parameters('loadBalancerName'), '/', parameters('loadBalancerInboundNatRuleNamePrefix'), 'ssh', copyIndex())]", 109 | "location": "[parameters('location')]", 110 | "copy": { 111 | "name": "lbNatLoop1", 112 | "count": "[parameters('numInboundNATRules')]" 113 | }, 114 | "dependsOn": [ 115 | "[concat('Microsoft.Network/loadBalancers/', parameters('loadBalancerName'))]" 116 | ], 117 | "properties": { 118 | "frontendIPConfiguration": { 119 | "id": "[variables('lbFrontEndIPConfigID')]" 120 | }, 121 | "protocol": "tcp", 122 | "frontendPort": "[copyIndex(parameters('inboundNATRuleSSHStartingPort'))]", 123 | "backendPort": "[parameters('inboundNATRuleSSHBackendPort')]", 124 | "enableFloatingIP": false 125 | } 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /nested/VMAuth-sshPublicKey.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "apiVersionVirtualMachines": { 6 | "type": "string" 7 | }, 8 | "apiVersionNetworkInterfaces": { 9 | "type": "string" 10 | }, 11 | "storagePerformance": { 12 | "type": "string" 13 | }, 14 | "loadBalancerName": { 15 | "type": "string" 16 | }, 17 | "loadBalancerBackendAddressPoolName": { 18 | "type": "string" 19 | }, 20 | "loadBalancerInboundNatRuleNamePrefix": { 21 | "type": "string" 22 | }, 23 | "subnetRef": { 24 | "type": "string" 25 | }, 26 | "vmNamePrefix": { 27 | "type": "string" 28 | }, 29 | "numVMs": { 30 | "type": "int" 31 | }, 32 | "offset": { 33 | "type": "int" 34 | }, 35 | "nicPrefix": { 36 | "type": "string" 37 | }, 38 | "availabilitySetName": { 39 | "type": "string" 40 | }, 41 | "vmSize": { 42 | "type": "string" 43 | }, 44 | "adminUsername": { 45 | "type": "string" 46 | }, 47 | "adminPassword": { 48 | "type": "securestring" 49 | }, 50 | "adminSSHKey": { 51 | "type": "string" 52 | }, 53 | "ubuntuImage": { 54 | "type": "object" 55 | }, 56 | "namingInfix": { 57 | "type": "string" 58 | }, 59 | "location": { 60 | "type": "string" 61 | } 62 | }, 63 | "variables": { 64 | "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", 65 | "loadBalancerID": "[resourceId('Microsoft.Network/loadBalancers', parameters('loadBalancerName'))]", 66 | "loadBalancerBackendAddressPoolID": "[concat(variables('loadBalancerID'), '/backendAddressPools/', parameters('loadBalancerBackendAddressPoolName'))]", 67 | "loadBalancerInboundNatRuleIDprefix": "[concat(variables('loadBalancerID'),'/inboundNatRules/',parameters('loadBalancerInboundNatRuleNamePrefix'))]" 68 | }, 69 | "resources": [ 70 | { 71 | "apiVersion": "[parameters('apiVersionNetworkInterfaces')]", 72 | "type": "Microsoft.Network/networkInterfaces", 73 | "name": "[concat(parameters('nicPrefix'), copyindex())]", 74 | "location": "[parameters('location')]", 75 | "copy": { 76 | "name": "txNicLoop", 77 | "count": "[parameters('numVMs')]" 78 | }, 79 | "properties": { 80 | "ipConfigurations": [{ 81 | "name": "ipconfig1", 82 | "properties": { 83 | "privateIPAllocationMethod": "Dynamic", 84 | "subnet": { 85 | "id": "[parameters('subnetRef')]" 86 | }, 87 | "loadBalancerBackendAddressPools": [ 88 | { 89 | "id": "[variables('loadBalancerBackendAddressPoolID')]" 90 | } 91 | ], 92 | "loadBalancerInboundNatRules": [ 93 | { 94 | "id": "[concat(variables('loadBalancerInboundNatRuleIDprefix'), 'ssh', add(parameters('offset'), copyindex()))]" 95 | } 96 | ] 97 | } 98 | }] 99 | } 100 | }, 101 | { 102 | "apiVersion": "[parameters('apiVersionVirtualMachines')]", 103 | "type": "Microsoft.Compute/virtualMachines", 104 | "name": "[concat(parameters('vmNamePrefix'), copyIndex())]", 105 | "location": "[parameters('location')]", 106 | "copy": { 107 | "name": "VMloop", 108 | "count": "[parameters('numVMs')]" 109 | }, 110 | "dependsOn": [ 111 | "[concat('Microsoft.Network/networkInterfaces/', parameters('nicPrefix'), copyIndex())]" 112 | ], 113 | "properties": { 114 | "availabilitySet": { 115 | "id": "[resourceId('Microsoft.Compute/availabilitySets',parameters('availabilitySetName'))]" 116 | }, 117 | "hardwareProfile": { 118 | "vmSize": "[parameters('vmSize')]" 119 | }, 120 | "osProfile": { 121 | "computerName": "[concat(parameters('vmNamePrefix'), copyIndex())]", 122 | "adminUsername": "[parameters('adminUsername')]", 123 | "linuxConfiguration": { 124 | "disablePasswordAuthentication": true, 125 | "ssh": { 126 | "publicKeys": [ 127 | { 128 | "path": "[variables('sshKeyPath')]", 129 | "keyData": "[parameters('adminSSHKey')]" 130 | } 131 | ] 132 | } 133 | } 134 | }, 135 | "storageProfile": { 136 | "imageReference": "[parameters('ubuntuImage')]", 137 | "osDisk": { 138 | "createOption": "FromImage", 139 | "managedDisk": { 140 | "storageAccountType": "[parameters('storagePerformance')]" 141 | } 142 | } 143 | }, 144 | "networkProfile": { 145 | "networkInterfaces": [ 146 | { 147 | "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(parameters('nicPrefix'), copyindex()))]" 148 | } 149 | ] 150 | } 151 | } 152 | } 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /configtx_template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ################################################################################ 3 | # 4 | # Profile - sample configuration for Azure deployment 5 | # 6 | # - Different configuration profiles may be encoded here to be specified 7 | # as parameters to the configtxgen tool 8 | # 9 | ################################################################################ 10 | Profiles: 11 | 12 | ComposerOrdererGenesis: 13 | Orderer: 14 | <<: *OrdererDefaults 15 | Organizations: 16 | - *OrdererOrg 17 | Consortiums: 18 | ComposerConsortium: 19 | Organizations: 20 | - *Org1 21 | ComposerChannel: 22 | Consortium: ComposerConsortium 23 | Application: 24 | <<: *ApplicationDefaults 25 | Organizations: 26 | - *Org1 27 | 28 | ################################################################################ 29 | # 30 | # Section: Organizations 31 | # 32 | # - This section defines the different organizational identities which will 33 | # be referenced later in the configuration. 34 | # 35 | ################################################################################ 36 | Organizations: 37 | 38 | # SampleOrg defines an MSP using the sampleconfig. It should never be used 39 | # in production but may be used as a template for other definitions 40 | - &OrdererOrg 41 | # DefaultOrg defines the organization which is used in the sampleconfig 42 | # of the fabric.git development environment 43 | Name: OrdererOrg 44 | 45 | # ID to load the MSP definition as 46 | ID: OrdererMSP 47 | 48 | # MSPDir is the filesystem path which contains the MSP configuration 49 | MSPDir: crypto-config/ordererOrganizations/fabienpe.com/msp 50 | 51 | # AdminPrincipal dictates the type of principal used for an 52 | # organization's Admins policy. Today, only the values of Role.ADMIN and 53 | # Role.MEMBER are accepted, which indicates a principal of role type 54 | # ADMIN and role type MEMBER respectively. 55 | AdminPrincipal: Role.ADMIN 56 | 57 | - &Org1 58 | # DefaultOrg defines the organization which is used in the sampleconfig 59 | # of the fabric.git development environment 60 | Name: Org1 61 | 62 | # ID to load the MSP definition as 63 | ID: Org1MSP 64 | 65 | # MSPDir is the filesystem path which contains the MSP configuration 66 | MSPDir: crypto-config/peerOrganizations/org1.fabienpe.com/msp 67 | 68 | # AdminPrincipal dictates the type of principal used for an 69 | # organization's Admins policy. Today, only the values of Role.ADMIN and 70 | # Role.MEMBER are accepted, which indicates a principal of role type 71 | # ADMIN and role type MEMBER respectively. 72 | AdminPrincipal: Role.ADMIN 73 | 74 | AnchorPeers: 75 | # AnchorPeers defines the location of peers which can be used 76 | # for cross org gossip communication. Note, this value is only 77 | # encoded in the genesis block in the Application section context 78 | - Host: {{PREFIX}}-peer0 79 | Port: 7051 80 | 81 | ################################################################################ 82 | # 83 | # SECTION: Orderer 84 | # 85 | # - This section defines the values to encode into a config transaction or 86 | # genesis block for orderer related parameters 87 | # 88 | ################################################################################ 89 | Orderer: &OrdererDefaults 90 | 91 | # Orderer Type: The orderer implementation to start 92 | # Available types are "solo" and "kafka" 93 | OrdererType: solo 94 | 95 | Addresses: 96 | - {{PREFIX}}-orderer0:7050 97 | 98 | # Batch Timeout: The amount of time to wait before creating a batch 99 | BatchTimeout: 2s 100 | 101 | # Batch Size: Controls the number of messages batched into a block 102 | BatchSize: 103 | 104 | # Max Message Count: The maximum number of messages to permit in a batch 105 | MaxMessageCount: 10 106 | 107 | # Absolute Max Bytes: The absolute maximum number of bytes allowed for 108 | # the serialized messages in a batch. 109 | AbsoluteMaxBytes: 99 MB 110 | 111 | # Preferred Max Bytes: The preferred maximum number of bytes allowed for 112 | # the serialized messages in a batch. A message larger than the preferred 113 | # max bytes will result in a batch larger than preferred max bytes. 114 | PreferredMaxBytes: 512 KB 115 | 116 | Kafka: 117 | # Brokers: A list of Kafka brokers to which the orderer connects 118 | # NOTE: Use IP:port notation 119 | Brokers: 120 | - orderer0:9092 121 | 122 | # Organizations is the list of orgs which are defined as participants on 123 | # the orderer side of the network 124 | Organizations: 125 | 126 | ################################################################################ 127 | # 128 | # SECTION: Application 129 | # 130 | # - This section defines the values to encode into a config transaction or 131 | # genesis block for application related parameters 132 | # 133 | ################################################################################ 134 | Application: &ApplicationDefaults 135 | 136 | # Organizations is the list of orgs which are defined as participants on 137 | # the application side of the network 138 | Organizations: 139 | -------------------------------------------------------------------------------- /nested/VMExtension.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "caVMNamePrefix": { 6 | "type": "string" 7 | }, 8 | "numMembershipNodes": { 9 | "type": "int" 10 | }, 11 | "ordererVMNamePrefix": { 12 | "type": "string" 13 | }, 14 | "numOrdererNodes": { 15 | "type": "int" 16 | }, 17 | "peerVMNamePrefix": { 18 | "type": "string" 19 | }, 20 | "numPeerNodes": { 21 | "type": "int" 22 | }, 23 | "adminUsername": { 24 | "type": "string" 25 | }, 26 | "adminSitePort": { 27 | "type": "int" 28 | }, 29 | "artifactsLocationURL": { 30 | "type": "string" 31 | }, 32 | "CAUsername": { 33 | "type": "string" 34 | }, 35 | "CAPassword": { 36 | "type": "securestring" 37 | }, 38 | "namingInfix": { 39 | "type": "string" 40 | }, 41 | "location": { 42 | "type": "string" 43 | } 44 | }, 45 | "variables": { 46 | "apiVersionVirtualMachinesExtensions": "2016-03-30" 47 | }, 48 | "resources": [ 49 | { 50 | "apiVersion": "[variables('apiVersionVirtualMachinesExtensions')]", 51 | "type": "Microsoft.Compute/virtualMachines/extensions", 52 | "name": "[concat(parameters('caVMNamePrefix'), copyIndex(), '/newuserscript')]", 53 | "copy": { 54 | "name": "caConfigLoop", 55 | "count": "[parameters('numMembershipNodes')]" 56 | }, 57 | "location": "[parameters('location')]", 58 | "properties": { 59 | "publisher": "Microsoft.Azure.Extensions", 60 | "type": "CustomScript", 61 | "typeHandlerVersion": "2.0", 62 | "autoUpgradeMinorVersion": true, 63 | "settings": { 64 | "fileUris": [ 65 | "[concat(parameters('artifactsLocationURL'), '/scripts/configure.sh')]" 66 | ] 67 | }, 68 | "protectedSettings": { 69 | "commandToExecute": "[concat('/bin/bash configure.sh \"ca\" \"', parameters('adminUsername'), '\" \"', parameters('artifactsLocationURL'), '\" \"', copyIndex(), '\" \"', parameters('caVMNamePrefix'), '\" \"', parameters('numMembershipNodes'), '\" \"', parameters('ordererVMNamePrefix'), '\" \"', parameters('numOrdererNodes'), '\" \"', parameters('peerVMNamePrefix'), '\" \"', parameters('numPeerNodes'), '\" \"', parameters('CAUsername'), '\" \"', parameters('CAPassword'), '\" \"', parameters('namingInfix'), '\"')]" 70 | } 71 | } 72 | }, 73 | { 74 | "apiVersion": "[variables('apiVersionVirtualMachinesExtensions')]", 75 | "type": "Microsoft.Compute/virtualMachines/extensions", 76 | "dependsOn": [ 77 | "[concat('Microsoft.Compute/virtualMachines/', parameters('caVMNamePrefix'), '0', '/extensions/newuserscript')]" 78 | ], 79 | "name": "[concat(parameters('ordererVMNamePrefix'), copyIndex(), '/newuserscript')]", 80 | "copy": { 81 | "name": "orderersConfigLoop", 82 | "count": "[parameters('numOrdererNodes')]" 83 | }, 84 | "location": "[parameters('location')]", 85 | "properties": { 86 | "publisher": "Microsoft.Azure.Extensions", 87 | "type": "CustomScript", 88 | "typeHandlerVersion": "2.0", 89 | "autoUpgradeMinorVersion": true, 90 | "settings": { 91 | "fileUris": [ 92 | "[concat(parameters('artifactsLocationURL'), '/scripts/configure.sh')]" 93 | ] 94 | }, 95 | "protectedSettings": { 96 | "commandToExecute": "[concat('/bin/bash configure.sh \"orderer\" \"', parameters('adminUsername'), '\" \"', parameters('artifactsLocationURL'), '\" \"', copyIndex(), '\" \"', parameters('caVMNamePrefix'), '\" \"', parameters('numMembershipNodes'), '\" \"', parameters('ordererVMNamePrefix'), '\" \"', parameters('numOrdererNodes'), '\" \"', parameters('peerVMNamePrefix'), '\" \"', parameters('numPeerNodes'), '\" \"', parameters('CAUsername'), '\" \"', parameters('CAPassword'), '\" \"', parameters('namingInfix'), '\"')]" 97 | } 98 | } 99 | }, 100 | { 101 | "apiVersion": "[variables('apiVersionVirtualMachinesExtensions')]", 102 | "type": "Microsoft.Compute/virtualMachines/extensions", 103 | "dependsOn": [ 104 | "[concat('Microsoft.Compute/virtualMachines/', parameters('caVMNamePrefix'), '0', '/extensions/newuserscript')]", 105 | "[concat('Microsoft.Compute/virtualMachines/', parameters('ordererVMNamePrefix'), '0', '/extensions/newuserscript')]" 106 | ], 107 | "name": "[concat(parameters('peerVMNamePrefix'), copyIndex(), '/newuserscript')]", 108 | "copy": { 109 | "name": "peersConfigLoop", 110 | "count": "[parameters('numPeerNodes')]" 111 | }, 112 | "location": "[parameters('location')]", 113 | "properties": { 114 | "publisher": "Microsoft.Azure.Extensions", 115 | "type": "CustomScript", 116 | "typeHandlerVersion": "2.0", 117 | "autoUpgradeMinorVersion": true, 118 | "settings": { 119 | "fileUris": [ 120 | "[concat(parameters('artifactsLocationURL'), '/scripts/configure.sh')]" 121 | ] 122 | }, 123 | "protectedSettings": { 124 | "commandToExecute": "[concat('/bin/bash configure.sh \"peer\" \"', parameters('adminUsername'), '\" \"', parameters('artifactsLocationURL'), '\" \"', copyIndex(), '\" \"', parameters('caVMNamePrefix'), '\" \"', parameters('numMembershipNodes'), '\" \"', parameters('ordererVMNamePrefix'), '\" \"', parameters('numOrdererNodes'), '\" \"', parameters('peerVMNamePrefix'), '\" \"', parameters('numPeerNodes'), '\" \"', parameters('CAUsername'), '\" \"', parameters('CAPassword'), '\" \"', parameters('namingInfix'), '\"')]" 125 | } 126 | } 127 | } 128 | ] 129 | } 130 | -------------------------------------------------------------------------------- /DeploymentHelper.cs: -------------------------------------------------------------------------------- 1 | // Requires the following Azure NuGet packages and related dependencies: 2 | // package id="Microsoft.Azure.Management.Authorization" version="2.0.0" 3 | // package id="Microsoft.Azure.Management.ResourceManager" version="1.4.0-preview" 4 | // package id="Microsoft.Rest.ClientRuntime.Azure.Authentication" version="2.2.8-preview" 5 | 6 | using Microsoft.Azure.Management.ResourceManager; 7 | using Microsoft.Azure.Management.ResourceManager.Models; 8 | using Microsoft.Rest.Azure.Authentication; 9 | using Newtonsoft.Json; 10 | using Newtonsoft.Json.Linq; 11 | using System; 12 | using System.IO; 13 | 14 | namespace PortalGenerated 15 | { 16 | /// 17 | /// This is a helper class for deploying an Azure Resource Manager template 18 | /// More info about template deployments can be found here https://go.microsoft.com/fwLink/?LinkID=733371 19 | /// 20 | class DeploymentHelper 21 | { 22 | string subscriptionId = "your-subscription-id"; 23 | string clientId = "your-service-principal-clientId"; 24 | string clientSecret = "your-service-principal-client-secret"; 25 | string resourceGroupName = "resource-group-name"; 26 | string deploymentName = "deployment-name"; 27 | string resourceGroupLocation = "resource-group-location"; // must be specified for creating a new resource group 28 | string pathToTemplateFile = "path-to-template.json-on-disk"; 29 | string pathToParameterFile = "path-to-parameters.json-on-disk"; 30 | string tenantId = "tenant-id"; 31 | 32 | public async void Run() 33 | { 34 | // Try to obtain the service credentials 35 | var serviceCreds = await ApplicationTokenProvider.LoginSilentAsync(tenantId, clientId, clientSecret); 36 | 37 | // Read the template and parameter file contents 38 | JObject templateFileContents = GetJsonFileContents(pathToTemplateFile); 39 | JObject parameterFileContents = GetJsonFileContents(pathToParameterFile); 40 | 41 | // Create the resource manager client 42 | var resourceManagementClient = new ResourceManagementClient(serviceCreds); 43 | resourceManagementClient.SubscriptionId = subscriptionId; 44 | 45 | // Create or check that resource group exists 46 | EnsureResourceGroupExists(resourceManagementClient, resourceGroupName, resourceGroupLocation); 47 | 48 | // Start a deployment 49 | DeployTemplate(resourceManagementClient, resourceGroupName, deploymentName, templateFileContents, parameterFileContents); 50 | } 51 | 52 | /// 53 | /// Reads a JSON file from the specified path 54 | /// 55 | /// The full path to the JSON file 56 | /// The JSON file contents 57 | private JObject GetJsonFileContents(string pathToJson) 58 | { 59 | JObject templatefileContent = new JObject(); 60 | using (StreamReader file = File.OpenText(pathToJson)) 61 | { 62 | using (JsonTextReader reader = new JsonTextReader(file)) 63 | { 64 | templatefileContent = (JObject)JToken.ReadFrom(reader); 65 | return templatefileContent; 66 | } 67 | } 68 | } 69 | 70 | /// 71 | /// Ensures that a resource group with the specified name exists. If it does not, will attempt to create one. 72 | /// 73 | /// The resource manager client. 74 | /// The name of the resource group. 75 | /// The resource group location. Required when creating a new resource group. 76 | private static void EnsureResourceGroupExists(ResourceManagementClient resourceManagementClient, string resourceGroupName, string resourceGroupLocation) 77 | { 78 | if (resourceManagementClient.ResourceGroups.CheckExistence(resourceGroupName) != true) 79 | { 80 | Console.WriteLine(string.Format("Creating resource group '{0}' in location '{1}'", resourceGroupName, resourceGroupLocation)); 81 | var resourceGroup = new ResourceGroup(); 82 | resourceGroup.Location = resourceGroupLocation; 83 | resourceManagementClient.ResourceGroups.CreateOrUpdate(resourceGroupName, resourceGroup); 84 | } 85 | else 86 | { 87 | Console.WriteLine(string.Format("Using existing resource group '{0}'", resourceGroupName)); 88 | } 89 | } 90 | 91 | /// 92 | /// Starts a template deployment. 93 | /// 94 | /// The resource manager client. 95 | /// The name of the resource group. 96 | /// The name of the deployment. 97 | /// The template file contents. 98 | /// The parameter file contents. 99 | private static void DeployTemplate(ResourceManagementClient resourceManagementClient, string resourceGroupName, string deploymentName, JObject templateFileContents, JObject parameterFileContents) 100 | { 101 | Console.WriteLine(string.Format("Starting template deployment '{0}' in resource group '{1}'", deploymentName, resourceGroupName)); 102 | var deployment = new Deployment(); 103 | 104 | deployment.Properties = new DeploymentProperties 105 | { 106 | Mode = DeploymentMode.Incremental, 107 | Template = templateFileContents, 108 | Parameters = parameterFileContents["parameters"].ToObject() 109 | }; 110 | 111 | var deploymentResult = resourceManagementClient.Deployments.CreateOrUpdate(resourceGroupName, deploymentName, deployment); 112 | Console.WriteLine(string.Format("Deployment status: {0}", deploymentResult.Properties.ProvisioningState)); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /tutorial/tutorial-network/features/sample.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | Feature: Sample 16 | 17 | Background: 18 | Given I have deployed the business network definition .. 19 | And I have added the following participants of type org.acme.mynetwork.SampleParticipant 20 | | participantId | firstName | lastName | 21 | | alice@email.com | Alice | A | 22 | | bob@email.com | Bob | B | 23 | And I have added the following assets of type org.acme.mynetwork.SampleAsset 24 | | assetId | owner | value | 25 | | 1 | alice@email.com | 10 | 26 | | 2 | bob@email.com | 20 | 27 | And I have issued the participant org.acme.mynetwork.SampleParticipant#alice@email.com with the identity alice1 28 | And I have issued the participant org.acme.mynetwork.SampleParticipant#bob@email.com with the identity bob1 29 | 30 | Scenario: Alice can read all of the assets 31 | When I use the identity alice1 32 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 33 | | assetId | owner | value | 34 | | 1 | alice@email.com | 10 | 35 | | 2 | bob@email.com | 20 | 36 | 37 | Scenario: Bob can read all of the assets 38 | When I use the identity alice1 39 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 40 | | assetId | owner | value | 41 | | 1 | alice@email.com | 10 | 42 | | 2 | bob@email.com | 20 | 43 | 44 | Scenario: Alice can add assets that she owns 45 | When I use the identity alice1 46 | And I add the following asset of type org.acme.mynetwork.SampleAsset 47 | | assetId | owner | value | 48 | | 3 | alice@email.com | 30 | 49 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 50 | | assetId | owner | value | 51 | | 3 | alice@email.com | 30 | 52 | 53 | Scenario: Alice cannot add assets that Bob owns 54 | When I use the identity alice1 55 | And I add the following asset of type org.acme.mynetwork.SampleAsset 56 | | assetId | owner | value | 57 | | 3 | bob@email.com | 30 | 58 | Then I should get an error matching /does not have .* access to resource/ 59 | 60 | Scenario: Bob can add assets that he owns 61 | When I use the identity bob1 62 | And I add the following asset of type org.acme.mynetwork.SampleAsset 63 | | assetId | owner | value | 64 | | 4 | bob@email.com | 40 | 65 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 66 | | assetId | owner | value | 67 | | 4 | bob@email.com | 40 | 68 | 69 | Scenario: Bob cannot add assets that Alice owns 70 | When I use the identity bob1 71 | And I add the following asset of type org.acme.mynetwork.SampleAsset 72 | | assetId | owner | value | 73 | | 4 | alice@email.com | 40 | 74 | Then I should get an error matching /does not have .* access to resource/ 75 | 76 | Scenario: Alice can update her assets 77 | When I use the identity alice1 78 | And I update the following asset of type org.acme.mynetwork.SampleAsset 79 | | assetId | owner | value | 80 | | 1 | alice@email.com | 50 | 81 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 82 | | assetId | owner | value | 83 | | 1 | alice@email.com | 50 | 84 | 85 | Scenario: Alice cannot update Bob's assets 86 | When I use the identity alice1 87 | And I update the following asset of type org.acme.mynetwork.SampleAsset 88 | | assetId | owner | value | 89 | | 2 | bob@email.com | 50 | 90 | Then I should get an error matching /does not have .* access to resource/ 91 | 92 | Scenario: Bob can update his assets 93 | When I use the identity bob1 94 | And I update the following asset of type org.acme.mynetwork.SampleAsset 95 | | assetId | owner | value | 96 | | 2 | bob@email.com | 60 | 97 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 98 | | assetId | owner | value | 99 | | 2 | bob@email.com | 60 | 100 | 101 | Scenario: Bob cannot update Alice's assets 102 | When I use the identity bob1 103 | And I update the following asset of type org.acme.mynetwork.SampleAsset 104 | | assetId | owner | value | 105 | | 1 | alice@email.com | 60 | 106 | Then I should get an error matching /does not have .* access to resource/ 107 | 108 | Scenario: Alice can remove her assets 109 | When I use the identity alice1 110 | And I remove the following asset of type org.acme.mynetwork.SampleAsset 111 | | assetId | 112 | | 1 | 113 | Then I should not have the following assets of type org.acme.mynetwork.SampleAsset 114 | | assetId | 115 | | 1 | 116 | 117 | Scenario: Alice cannot remove Bob's assets 118 | When I use the identity alice1 119 | And I remove the following asset of type org.acme.mynetwork.SampleAsset 120 | | assetId | 121 | | 2 | 122 | Then I should get an error matching /does not have .* access to resource/ 123 | 124 | Scenario: Bob can remove his assets 125 | When I use the identity bob1 126 | And I remove the following asset of type org.acme.mynetwork.SampleAsset 127 | | assetId | 128 | | 2 | 129 | Then I should not have the following assets of type org.acme.mynetwork.SampleAsset 130 | | assetId | 131 | | 2 | 132 | 133 | Scenario: Bob cannot remove Alice's assets 134 | When I use the identity bob1 135 | And I remove the following asset of type org.acme.mynetwork.SampleAsset 136 | | assetId | 137 | | 1 | 138 | Then I should get an error matching /does not have .* access to resource/ 139 | 140 | Scenario: Alice can submit a transaction for her assets 141 | When I use the identity alice1 142 | And I submit the following transaction of type org.acme.mynetwork.SampleTransaction 143 | | asset | newValue | 144 | | 1 | 50 | 145 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 146 | | assetId | owner | value | 147 | | 1 | alice@email.com | 50 | 148 | And I should have received the following event of type org.acme.mynetwork.SampleEvent 149 | | asset | oldValue | newValue | 150 | | 1 | 10 | 50 | 151 | 152 | Scenario: Alice cannot submit a transaction for Bob's assets 153 | When I use the identity alice1 154 | And I submit the following transaction of type org.acme.mynetwork.SampleTransaction 155 | | asset | newValue | 156 | | 2 | 50 | 157 | Then I should get an error matching /does not have .* access to resource/ 158 | 159 | Scenario: Bob can submit a transaction for his assets 160 | When I use the identity bob1 161 | And I submit the following transaction of type org.acme.mynetwork.SampleTransaction 162 | | asset | newValue | 163 | | 2 | 60 | 164 | Then I should have the following assets of type org.acme.mynetwork.SampleAsset 165 | | assetId | owner | value | 166 | | 2 | bob@email.com | 60 | 167 | And I should have received the following event of type org.acme.mynetwork.SampleEvent 168 | | asset | oldValue | newValue | 169 | | 2 | 20 | 60 | 170 | 171 | Scenario: Bob cannot submit a transaction for Alice's assets 172 | When I use the identity bob1 173 | And I submit the following transaction of type org.acme.mynetwork.SampleTransaction 174 | | asset | newValue | 175 | | 1 | 60 | 176 | Then I should get an error matching /does not have .* access to resource/ 177 | -------------------------------------------------------------------------------- /scripts/configure-fabric-azureuser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | unsuccessful_exit() 4 | { 5 | echo "FATAL: Exiting script due to: $1. Exit code: $2"; 6 | exit $2; 7 | } 8 | 9 | NODE_TYPE=$1 # ca, orderer, peer 10 | AZUREUSER=$2 11 | ARTIFACTS_URL_PREFIX=$3 12 | NODE_INDEX=$4 13 | CA_PREFIX=$5 14 | CA_NUM=$6 15 | ORDERER_PREFIX=$7 16 | ORDERER_NUM=$8 17 | PEER_PREFIX=$9 18 | PEER_NUM=${10} 19 | CA_USER=${11} 20 | CA_PASSWORD=${12} 21 | AZURE_PREFIX=${13} 22 | FABRIC_VERSION=x86_64-1.1.0 23 | 24 | # TODO: extract those from the configuration 25 | PEER_ORG_DOMAIN="org1.fabienpe.com" 26 | ORDERER_ORG_DOMAIN="fabienpe.com" 27 | 28 | echo "Configuring node:" 29 | echo " NODE_TYPE="${NODE_TYPE} 30 | echo " AZUREUSER="${AZUREUSER} 31 | echo " ARTIFACTS_URL_PREFIX="${ARTIFACTS_URL_PREFIX} 32 | echo " NODE_INDEX="${NODE_INDEX} 33 | echo " CA_PREFIX="${CA_PREFIX} 34 | echo " CA_NUM="${CA_NUM} 35 | echo " ORDERER_PREFIX="${ORDERER_PREFIX} 36 | echo " ORDERER_NUM="${ORDERER_NUM} 37 | echo " PEER_PREFIX="${PEER_PREFIX} 38 | echo " PEER_NUM="${PEER_NUM} 39 | echo " CA_USER="${CA_USER} 40 | echo " CA_PASSWORD="${CA_PASSWORD} 41 | echo " AZURE_PREFIX="${AZURE_PREFIX} 42 | echo " FABRIC_VERSION="${FABRIC_VERSION} 43 | echo " PEER_ORG_DOMAIN="${PEER_ORG_DOMAIN} 44 | echo " ORDERER_ORG_DOMAIN="${ORDERER_ORG_DOMAIN} 45 | 46 | 47 | function generate_artifacts { 48 | echo "############################################################" 49 | echo "Generating network artifacts..." 50 | 51 | # Retrieve configuration templates 52 | wget -N ${ARTIFACTS_URL_PREFIX}/configtx_template.yaml 53 | wget -N ${ARTIFACTS_URL_PREFIX}/crypto-config_template.yaml 54 | 55 | # Retrieve binaries 56 | curl -qL https://nexus.hyperledger.org/content/repositories/releases/org/hyperledger/fabric/hyperledger-fabric/linux-amd64-1.1.0/hyperledger-fabric-linux-amd64-1.1.0.tar.gz -o hyperledger-fabric-linux-amd64-1.1.0.tar.gz 57 | tar -xvf hyperledger-fabric-linux-amd64-1.1.0.tar.gz || unsuccessful_exit "Failed to retrieve binaries" 203 58 | 59 | # Set up environment 60 | os_arch=$(echo "$(uname -s)-amd64" | awk '{print tolower($0)}') 61 | export FABRIC_CFG_PATH=$PWD 62 | 63 | # Parse configuration templates 64 | sed -e "s/{{PREFIX}}/${AZURE_PREFIX}/g" -e "s/{{PEER_NUM}}/${PEER_NUM}/g" crypto-config_template.yaml > crypto-config.yaml 65 | sed -e "s/{{PREFIX}}/${AZURE_PREFIX}/g" configtx_template.yaml > configtx.yaml 66 | 67 | # Generate crypto config 68 | ./bin/cryptogen generate --config=./crypto-config.yaml || unsuccessful_exit "Failed to generate crypto config" 204 69 | 70 | # Generate genesis block 71 | ./bin/configtxgen -profile ComposerOrdererGenesis -outputBlock orderer.block || unsuccessful_exit "Failed to generate orderer genesis block" 205 72 | 73 | # Generate transaction configuration 74 | ./bin/configtxgen -profile ComposerChannel -outputCreateChannelTx composerchannel.tx -channelID composerchannel || unsuccessful_exit "Failed to generate transaction channel" 206 75 | 76 | # Generate anchor peer update for Org1MSP 77 | ./bin/configtxgen -profile ComposerChannel -outputAnchorPeersUpdate Org1MSPanchors.tx -channelID composerchannel -asOrg Org1 || unsuccessful_exit "Failed to generate anchor peer update for Org1" 207 78 | } 79 | 80 | function get_artifacts { 81 | echo "############################################################" 82 | echo "Retrieving network artifacts..." 83 | 84 | # Copy the artifacts from the first CA host 85 | scp -o StrictHostKeyChecking=no "${CA_PREFIX}0:~/configtx.yaml" . || unsuccessful_exit "Failed to retrieve configtx.yaml" 208 86 | scp -o StrictHostKeyChecking=no "${CA_PREFIX}0:~/orderer.block" . || unsuccessful_exit "Failed to retrieve orderer.block" 209 87 | scp -o StrictHostKeyChecking=no "${CA_PREFIX}0:~/composerchannel.tx" . || unsuccessful_exit "Failed to retrieve composerchannel.tx" 210 88 | scp -o StrictHostKeyChecking=no -r "${CA_PREFIX}0:~/crypto-config" . || unsuccessful_exit "Failed to retrieve crypto-config" 211 89 | 90 | echo "############################################################" 91 | } 92 | 93 | function distribute_ssh_key { 94 | echo "############################################################" 95 | echo "Generating ssh key..." 96 | 97 | # Generate new ssh key pair 98 | ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa || unsuccessful_exit "Failed to generate ssh key" 212 99 | 100 | # Authorize new key 101 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 102 | chmod 700 ~/.ssh 103 | chmod 600 ~/.ssh/authorized_keys 104 | 105 | # Expose private key to other nodes 106 | while true; do echo -e "HTTP/1.1 200 OK\n\n$(cat ~/.ssh/id_rsa)" | nc -l -p 1515; done & 107 | 108 | echo "############################################################" 109 | } 110 | 111 | function get_ssh_key { 112 | echo "############################################################" 113 | echo "Retrieving ssh key..." 114 | 115 | # Get the ssh key from the first CA host 116 | # TODO: loop here waiting for the request to succeed, instead of sequencing via the template dependencies? 117 | curl "http://${CA_PREFIX}0:1515/" -o ~/.ssh/id_rsa || unsuccessful_exit "Failed to retrieve ssh key" 201 118 | 119 | # Fix permissions 120 | chmod 700 ~/.ssh 121 | chmod 600 ~/.ssh/id_rsa 122 | 123 | echo "############################################################" 124 | } 125 | 126 | function install_composer { 127 | # Install composer and its prerequisites on the VM running Hyperledger CA 128 | echo "############################################################" 129 | echo "Installing composer development tools..." 130 | 131 | # Execute nvm installation script 132 | echo "# Executing nvm installation script" 133 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash 134 | 135 | # Set up nvm environment without restarting the shell 136 | export NVM_DIR="${HOME}/.nvm" 137 | [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh" 138 | [ -s "${NVM_DIR}/bash_completion" ] && . "${NVM_DIR}/bash_completion" 139 | 140 | # Install nodeJS 141 | echo "# Installing nodeJS" 142 | nvm install --lts 143 | 144 | # Configure nvm to use version 6.9.5 145 | nvm use --lts 146 | nvm alias default 'lts/*' 147 | 148 | # Install the latest version of npm 149 | echo "# Installing npm" 150 | npm install npm@latest -g 151 | 152 | # Log installation details for user 153 | echo -n 'Node: ' 154 | node --version 155 | echo -n 'npm: ' 156 | npm --version 157 | echo -n 'Docker: ' 158 | docker --version 159 | echo -n 'Python: ' 160 | python -V 161 | 162 | # Install development environment 163 | # https://hyperledger.github.io/composer/latest/installing/development-tools.html 164 | 165 | # Install CLI tools 166 | npm install -g composer-cli || unsuccessful_exit "Failed to install composer-cli" 200 167 | npm install -g composer-rest-server || unsuccessful_exit "Failed to install composer-rest-server" 201 168 | npm install -g generator-hyperledger-composer || unsuccessful_exit "Failed to install generator-hyperledger-composer" 202 169 | npm install -g yo || unsuccessful_exit "Failed to install yo" 203 170 | 171 | # Install Hyperledger Fabric tools 172 | # mkdir ~/fabric-tools && cd ~/fabric-tools 173 | # curl -O https://raw.githubusercontent.com/hyperledger/composer-tools/master/packages/fabric-dev-servers/fabric-dev-servers.zip 174 | # unzip fabric-dev-servers.zip || unsuccessful_exit "Failed to retrieve binaries" 204 175 | # export FABRIC_VERSION=hlfv11 176 | # ./downloadFabric.sh 177 | 178 | echo "############################################################" 179 | } 180 | 181 | function prepare_tutorial { 182 | echo "############################################################" 183 | echo "Preparing tutorial script and parameters..." 184 | 185 | cd $HOME 186 | curl -qL ${ARTIFACTS_URL_PREFIX}/tutorial/run_tutorial_template.sh \ 187 | -o run_tutorial_template.sh 188 | 189 | sed -e "s/{{PEER_ORG_DOMAIN}}/${PEER_ORG_DOMAIN}/g" \ 190 | -e "s/{{CA_USER}}/${CA_USER}/g" \ 191 | -e "s/{{CA_PASSWORD}}/${CA_PASSWORD}/g" \ 192 | -e "s/{{PEER_PREFIX}}/${PEER_PREFIX}/g" \ 193 | run_tutorial_template.sh > run_tutorial.sh 194 | 195 | chmod u+x run_tutorial.sh 196 | 197 | curl -qL ${ARTIFACTS_URL_PREFIX}/tutorial/tutorial-network.zip \ 198 | -o $HOME/tutorial-network.zip 199 | 200 | cd $HOME 201 | unzip tutorial-network.zip 202 | cd tutorial-network 203 | 204 | sed -e "s/{{PEER_PREFIX}}/${PEER_PREFIX}/g" \ 205 | -e "s/{{CA_PREFIX}}/${CA_PREFIX}/g" \ 206 | -e "s/{{ORDERER_PREFIX}}/${ORDERER_PREFIX}/g" \ 207 | -e "s/{{PEER_ORG_DOMAIN}}/${PEER_ORG_DOMAIN}/g" \ 208 | -e "s/{{ORDERER_ORG_DOMAIN}}/${ORDERER_ORG_DOMAIN}/g" \ 209 | connection_template.json > connection.json 210 | 211 | echo "############################################################" 212 | } 213 | 214 | function install_ca { 215 | echo "############################################################" 216 | echo "Installing Membership Service..." 217 | 218 | cacert="/etc/hyperledger/fabric-ca-server-config/ca.${PEER_ORG_DOMAIN}-cert.pem" 219 | cakey="/etc/hyperledger/fabric-ca-server-config/$(basename crypto-config/peerOrganizations/${PEER_ORG_DOMAIN}/ca/*_sk)" 220 | 221 | # Pull Docker image 222 | docker pull hyperledger/fabric-ca:${FABRIC_VERSION} || unsuccessful_exit "Failed to pull docker CA image" 213 223 | 224 | # Start CA 225 | docker run --name ${CA_PREFIX}0.${ORDERER_ORG_DOMAIN} -d --restart=always -p 7054:7054 \ 226 | -e CORE_LOGGING_LEVEL=debug \ 227 | -e FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server \ 228 | -e FABRIC_CA_SERVER_CA_NAME=${CA_PREFIX}0.${PEER_ORG_DOMAIN} \ 229 | -v $HOME/crypto-config/peerOrganizations/${PEER_ORG_DOMAIN}/ca/:/etc/hyperledger/fabric-ca-server-config \ 230 | hyperledger/fabric-ca:${FABRIC_VERSION} \ 231 | fabric-ca-server start --ca.certfile $cacert --ca.keyfile $cakey -b "${CA_USER}":"${CA_PASSWORD}" -d \ 232 | || unsuccessful_exit "Failed to start CA" 214 233 | 234 | echo "############################################################" 235 | } 236 | 237 | function install_orderer { 238 | echo "############################################################" 239 | echo "Installing Orderer..." 240 | 241 | # Pull Docker image 242 | docker pull hyperledger/fabric-orderer:${FABRIC_VERSION} 243 | 244 | # Start Orderer 245 | docker run --name ${ORDERER_PREFIX}0.${ORDERER_ORG_DOMAIN} -d --restart=always -p 7050:7050 \ 246 | -e ORDERER_GENERAL_LOGLEVEL=debug \ 247 | -e ORDERER_GENERAL_LISTENADDRESS=0.0.0.0 \ 248 | -e ORDERER_GENERAL_GENESISMETHOD=file \ 249 | -e ORDERER_GENERAL_GENESISFILE=/etc/hyperledger/configtx/orderer.block \ 250 | -e ORDERER_GENERAL_LOCALMSPID=OrdererMSP \ 251 | -e ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/msp/orderer/msp \ 252 | -v $HOME/:/etc/hyperledger/configtx \ 253 | -v $HOME/crypto-config/ordererOrganizations/${ORDERER_ORG_DOMAIN}/orderers/${ORDERER_PREFIX}0.${ORDERER_ORG_DOMAIN}/msp:/etc/hyperledger/msp/orderer/msp \ 254 | hyperledger/fabric-orderer:${FABRIC_VERSION} orderer || unsuccessful_exit "Failed to start orderer" 215 255 | 256 | echo "############################################################" 257 | } 258 | 259 | function install_peer { 260 | echo "############################################################" 261 | echo "Installing Peer..." 262 | 263 | # Pull Docker image 264 | docker pull hyperledger/fabric-peer:${FABRIC_VERSION} 265 | 266 | # The Peer needs this image to cerate chaincode containers 267 | docker pull hyperledger/fabric-ccenv:${FABRIC_VERSION} 268 | 269 | # Start Peer 270 | #-e CORE_CHAINCODE_LOGGING_LEVEL=DEBUG \ 271 | #-e CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=composer_default \ 272 | #-v $HOME/configtx.yaml:/etc/hyperledger/fabric/configtx.yaml \ 273 | docker run --name ${PEER_PREFIX}${NODE_INDEX}.${PEER_ORG_DOMAIN} -d --restart=always -p 7051:7051 -p 7053:7053 \ 274 | -e CORE_LOGGING_LEVEL=debug \ 275 | -e CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock \ 276 | -e CORE_PEER_ID=${PEER_PREFIX}${NODE_INDEX}.${PEER_ORG_DOMAIN} \ 277 | -e CORE_PEER_LOCALMSPID=Org1MSP \ 278 | -e CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/msp \ 279 | -v /var/run:/host/var/run \ 280 | -v $HOME/:/etc/hyperledger/configtx \ 281 | -v $HOME/crypto-config/peerOrganizations/${PEER_ORG_DOMAIN}/peers/${PEER_PREFIX}${NODE_INDEX}.${PEER_ORG_DOMAIN}/msp:/etc/hyperledger/peer/msp \ 282 | -v $HOME/crypto-config/peerOrganizations/${PEER_ORG_DOMAIN}/users:/etc/hyperledger/msp/users \ 283 | hyperledger/fabric-peer:${FABRIC_VERSION} peer node start || unsuccessful_exit "Failed to start peer" 216 284 | 285 | # Commands to execute on one of the peers 286 | echo ${PEER_PREFIX}${NODE_INDEX}" started" 287 | if [ ${NODE_INDEX} -eq 0 ]; then 288 | echo "Creating channel..." 289 | echo "# Waiting 5 minutes." 290 | sleep 5m 291 | 292 | docker run -d --name CLI -v $HOME/crypto-config:/crypto-config \ 293 | -v $HOME/composerchannel.tx:/composerchannel.tx \ 294 | hyperledger/fabric-peer:x86_64-1.1.0; 295 | 296 | # docker exec CLI bash -c ' ; ' 297 | docker exec CLI bash -c 'export CORE_PEER_ADDRESS="'${PEER_PREFIX}'0:7051"; \ 298 | export CORE_PEER_LOCALMSPID="Org1MSP"; \ 299 | export CORE_PEER_MSPCONFIGPATH=/crypto-config/peerOrganizations/'${PEER_ORG_DOMAIN}'/users/Admin@'${PEER_ORG_DOMAIN}'/msp; \ 300 | peer channel create -o '${ORDERER_PREFIX}'0:7050 -c composerchannel -f composerchannel.tx' \ 301 | || unsuccessful_exit "Failed to create channel" 2010; 302 | 303 | docker exec CLI bash -c 'export CORE_PEER_ADDRESS="'${PEER_PREFIX}'0:7051"; \ 304 | export CORE_PEER_LOCALMSPID="Org1MSP"; \ 305 | export CORE_PEER_MSPCONFIGPATH=/crypto-config/peerOrganizations/'${PEER_ORG_DOMAIN}'/users/Admin@'${PEER_ORG_DOMAIN}'/msp; \ 306 | peer channel join -b composerchannel.block' \ 307 | || unsuccessful_exit "Failed to join channel (${PEER_PREFIX}0)" 2011; 308 | 309 | docker exec CLI bash -c 'export CORE_PEER_ADDRESS='${PEER_PREFIX}'1:7051; \ 310 | export CORE_PEER_LOCALMSPID="Org1MSP"; \ 311 | export CORE_PEER_MSPCONFIGPATH=/crypto-config/peerOrganizations/'${PEER_ORG_DOMAIN}'/users/Admin@'${PEER_ORG_DOMAIN}'/msp; \ 312 | peer channel join -b composerchannel.block -o '${ORDERER_PREFIX}'0:7050'\ 313 | || unsuccessful_exit "Failed to join channel (${PEER_PREFIX}1)" 2011; 314 | fi 315 | 316 | echo "############################################################" 317 | } 318 | 319 | 320 | # Jump to node-specific steps 321 | 322 | case "${NODE_TYPE}" in 323 | "ca") 324 | generate_artifacts 325 | distribute_ssh_key 326 | install_ca 327 | install_composer 328 | prepare_tutorial 329 | ;; 330 | "orderer") 331 | get_ssh_key 332 | get_artifacts 333 | install_orderer 334 | ;; 335 | "peer") 336 | get_ssh_key 337 | get_artifacts 338 | install_peer 339 | ;; 340 | *) 341 | unsuccessful_exit "Invalid node type, exiting." 202 342 | exit 202 343 | ;; 344 | esac 345 | -------------------------------------------------------------------------------- /tutorial/tutorial-network/test/logic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | 'use strict'; 16 | /** 17 | * Write the unit tests for your transction processor functions here 18 | */ 19 | 20 | const AdminConnection = require('composer-admin').AdminConnection; 21 | const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; 22 | const { BusinessNetworkDefinition, CertificateUtil, IdCard } = require('composer-common'); 23 | const path = require('path'); 24 | 25 | const chai = require('chai'); 26 | chai.should(); 27 | chai.use(require('chai-as-promised')); 28 | 29 | const namespace = 'org.acme.mynetwork'; 30 | const assetType = 'SampleAsset'; 31 | const assetNS = namespace + '.' + assetType; 32 | const participantType = 'SampleParticipant'; 33 | const participantNS = namespace + '.' + participantType; 34 | 35 | describe('#' + namespace, () => { 36 | // In-memory card store for testing so cards are not persisted to the file system 37 | const cardStore = require('composer-common').NetworkCardStoreManager.getCardStore( { type: 'composer-wallet-inmemory' } ); 38 | 39 | // Embedded connection used for local testing 40 | const connectionProfile = { 41 | name: 'embedded', 42 | 'x-type': 'embedded' 43 | }; 44 | 45 | // Name of the business network card containing the administrative identity for the business network 46 | const adminCardName = 'admin'; 47 | 48 | // Admin connection to the blockchain, used to deploy the business network 49 | let adminConnection; 50 | 51 | // This is the business network connection the tests will use. 52 | let businessNetworkConnection; 53 | 54 | // This is the factory for creating instances of types. 55 | let factory; 56 | 57 | // These are the identities for Alice and Bob. 58 | const aliceCardName = 'alice'; 59 | const bobCardName = 'bob'; 60 | 61 | // These are a list of receieved events. 62 | let events; 63 | 64 | let businessNetworkName; 65 | 66 | before(async () => { 67 | // Generate certificates for use with the embedded connection 68 | const credentials = CertificateUtil.generate({ commonName: 'admin' }); 69 | 70 | // Identity used with the admin connection to deploy business networks 71 | const deployerMetadata = { 72 | version: 1, 73 | userName: 'PeerAdmin', 74 | roles: [ 'PeerAdmin', 'ChannelAdmin' ] 75 | }; 76 | const deployerCard = new IdCard(deployerMetadata, connectionProfile); 77 | deployerCard.setCredentials(credentials); 78 | const deployerCardName = 'PeerAdmin'; 79 | 80 | adminConnection = new AdminConnection({ cardStore: cardStore }); 81 | 82 | await adminConnection.importCard(deployerCardName, deployerCard); 83 | await adminConnection.connect(deployerCardName); 84 | }); 85 | 86 | /** 87 | * 88 | * @param {String} cardName The card name to use for this identity 89 | * @param {Object} identity The identity details 90 | */ 91 | async function importCardForIdentity(cardName, identity) { 92 | const metadata = { 93 | userName: identity.userID, 94 | version: 1, 95 | enrollmentSecret: identity.userSecret, 96 | businessNetwork: businessNetworkName 97 | }; 98 | const card = new IdCard(metadata, connectionProfile); 99 | await adminConnection.importCard(cardName, card); 100 | } 101 | 102 | // This is called before each test is executed. 103 | beforeEach(async () => { 104 | // Generate a business network definition from the project directory. 105 | let businessNetworkDefinition = await BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..')); 106 | businessNetworkName = businessNetworkDefinition.getName(); 107 | await adminConnection.install(businessNetworkDefinition); 108 | const startOptions = { 109 | networkAdmins: [ 110 | { 111 | userName: 'admin', 112 | enrollmentSecret: 'adminpw' 113 | } 114 | ] 115 | }; 116 | const adminCards = await adminConnection.start(businessNetworkName, businessNetworkDefinition.getVersion(), startOptions); 117 | await adminConnection.importCard(adminCardName, adminCards.get('admin')); 118 | 119 | // Create and establish a business network connection 120 | businessNetworkConnection = new BusinessNetworkConnection({ cardStore: cardStore }); 121 | events = []; 122 | businessNetworkConnection.on('event', event => { 123 | events.push(event); 124 | }); 125 | await businessNetworkConnection.connect(adminCardName); 126 | 127 | // Get the factory for the business network. 128 | factory = businessNetworkConnection.getBusinessNetwork().getFactory(); 129 | 130 | const participantRegistry = await businessNetworkConnection.getParticipantRegistry(participantNS); 131 | // Create the participants. 132 | const alice = factory.newResource(namespace, participantType, 'alice@email.com'); 133 | alice.firstName = 'Alice'; 134 | alice.lastName = 'A'; 135 | 136 | const bob = factory.newResource(namespace, participantType, 'bob@email.com'); 137 | bob.firstName = 'Bob'; 138 | bob.lastName = 'B'; 139 | 140 | participantRegistry.addAll([alice, bob]); 141 | 142 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 143 | // Create the assets. 144 | const asset1 = factory.newResource(namespace, assetType, '1'); 145 | asset1.owner = factory.newRelationship(namespace, participantType, 'alice@email.com'); 146 | asset1.value = '10'; 147 | 148 | const asset2 = factory.newResource(namespace, assetType, '2'); 149 | asset2.owner = factory.newRelationship(namespace, participantType, 'bob@email.com'); 150 | asset2.value = '20'; 151 | 152 | assetRegistry.addAll([asset1, asset2]); 153 | 154 | // Issue the identities. 155 | let identity = await businessNetworkConnection.issueIdentity(participantNS + '#alice@email.com', 'alice1'); 156 | await importCardForIdentity(aliceCardName, identity); 157 | identity = await businessNetworkConnection.issueIdentity(participantNS + '#bob@email.com', 'bob1'); 158 | await importCardForIdentity(bobCardName, identity); 159 | }); 160 | 161 | /** 162 | * Reconnect using a different identity. 163 | * @param {String} cardName The name of the card for the identity to use 164 | */ 165 | async function useIdentity(cardName) { 166 | await businessNetworkConnection.disconnect(); 167 | businessNetworkConnection = new BusinessNetworkConnection({ cardStore: cardStore }); 168 | events = []; 169 | businessNetworkConnection.on('event', (event) => { 170 | events.push(event); 171 | }); 172 | await businessNetworkConnection.connect(cardName); 173 | factory = businessNetworkConnection.getBusinessNetwork().getFactory(); 174 | } 175 | 176 | it('Alice can read all of the assets', async () => { 177 | // Use the identity for Alice. 178 | await useIdentity(aliceCardName); 179 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 180 | const assets = await assetRegistry.getAll(); 181 | 182 | // Validate the assets. 183 | assets.should.have.lengthOf(2); 184 | const asset1 = assets[0]; 185 | asset1.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#alice@email.com'); 186 | asset1.value.should.equal('10'); 187 | const asset2 = assets[1]; 188 | asset2.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#bob@email.com'); 189 | asset2.value.should.equal('20'); 190 | }); 191 | 192 | it('Bob can read all of the assets', async () => { 193 | // Use the identity for Bob. 194 | await useIdentity(bobCardName); 195 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 196 | const assets = await assetRegistry.getAll(); 197 | 198 | // Validate the assets. 199 | assets.should.have.lengthOf(2); 200 | const asset1 = assets[0]; 201 | asset1.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#alice@email.com'); 202 | asset1.value.should.equal('10'); 203 | const asset2 = assets[1]; 204 | asset2.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#bob@email.com'); 205 | asset2.value.should.equal('20'); 206 | }); 207 | 208 | it('Alice can add assets that she owns', async () => { 209 | // Use the identity for Alice. 210 | await useIdentity(aliceCardName); 211 | 212 | // Create the asset. 213 | let asset3 = factory.newResource(namespace, assetType, '3'); 214 | asset3.owner = factory.newRelationship(namespace, participantType, 'alice@email.com'); 215 | asset3.value = '30'; 216 | 217 | // Add the asset, then get the asset. 218 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 219 | await assetRegistry.add(asset3); 220 | 221 | // Validate the asset. 222 | asset3 = await assetRegistry.get('3'); 223 | asset3.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#alice@email.com'); 224 | asset3.value.should.equal('30'); 225 | }); 226 | 227 | it('Alice cannot add assets that Bob owns', async () => { 228 | // Use the identity for Alice. 229 | await useIdentity(aliceCardName); 230 | 231 | // Create the asset. 232 | const asset3 = factory.newResource(namespace, assetType, '3'); 233 | asset3.owner = factory.newRelationship(namespace, participantType, 'bob@email.com'); 234 | asset3.value = '30'; 235 | 236 | // Try to add the asset, should fail. 237 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 238 | assetRegistry.add(asset3).should.be.rejectedWith(/does not have .* access to resource/); 239 | }); 240 | 241 | it('Bob can add assets that he owns', async () => { 242 | // Use the identity for Bob. 243 | await useIdentity(bobCardName); 244 | 245 | // Create the asset. 246 | let asset4 = factory.newResource(namespace, assetType, '4'); 247 | asset4.owner = factory.newRelationship(namespace, participantType, 'bob@email.com'); 248 | asset4.value = '40'; 249 | 250 | // Add the asset, then get the asset. 251 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 252 | await assetRegistry.add(asset4); 253 | 254 | // Validate the asset. 255 | asset4 = await assetRegistry.get('4'); 256 | asset4.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#bob@email.com'); 257 | asset4.value.should.equal('40'); 258 | }); 259 | 260 | it('Bob cannot add assets that Alice owns', async () => { 261 | // Use the identity for Bob. 262 | await useIdentity(bobCardName); 263 | 264 | // Create the asset. 265 | const asset4 = factory.newResource(namespace, assetType, '4'); 266 | asset4.owner = factory.newRelationship(namespace, participantType, 'alice@email.com'); 267 | asset4.value = '40'; 268 | 269 | // Try to add the asset, should fail. 270 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 271 | assetRegistry.add(asset4).should.be.rejectedWith(/does not have .* access to resource/); 272 | 273 | }); 274 | 275 | it('Alice can update her assets', async () => { 276 | // Use the identity for Alice. 277 | await useIdentity(aliceCardName); 278 | 279 | // Create the asset. 280 | let asset1 = factory.newResource(namespace, assetType, '1'); 281 | asset1.owner = factory.newRelationship(namespace, participantType, 'alice@email.com'); 282 | asset1.value = '50'; 283 | 284 | // Update the asset, then get the asset. 285 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 286 | await assetRegistry.update(asset1); 287 | 288 | // Validate the asset. 289 | asset1 = await assetRegistry.get('1'); 290 | asset1.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#alice@email.com'); 291 | asset1.value.should.equal('50'); 292 | }); 293 | 294 | it('Alice cannot update Bob\'s assets', async () => { 295 | // Use the identity for Alice. 296 | await useIdentity(aliceCardName); 297 | 298 | // Create the asset. 299 | const asset2 = factory.newResource(namespace, assetType, '2'); 300 | asset2.owner = factory.newRelationship(namespace, participantType, 'bob@email.com'); 301 | asset2.value = '50'; 302 | 303 | // Try to update the asset, should fail. 304 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 305 | assetRegistry.update(asset2).should.be.rejectedWith(/does not have .* access to resource/); 306 | }); 307 | 308 | it('Bob can update his assets', async () => { 309 | // Use the identity for Bob. 310 | await useIdentity(bobCardName); 311 | 312 | // Create the asset. 313 | let asset2 = factory.newResource(namespace, assetType, '2'); 314 | asset2.owner = factory.newRelationship(namespace, participantType, 'bob@email.com'); 315 | asset2.value = '60'; 316 | 317 | // Update the asset, then get the asset. 318 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 319 | await assetRegistry.update(asset2); 320 | 321 | // Validate the asset. 322 | asset2 = await assetRegistry.get('2'); 323 | asset2.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#bob@email.com'); 324 | asset2.value.should.equal('60'); 325 | }); 326 | 327 | it('Bob cannot update Alice\'s assets', async () => { 328 | // Use the identity for Bob. 329 | await useIdentity(bobCardName); 330 | 331 | // Create the asset. 332 | const asset1 = factory.newResource(namespace, assetType, '1'); 333 | asset1.owner = factory.newRelationship(namespace, participantType, 'alice@email.com'); 334 | asset1.value = '60'; 335 | 336 | // Update the asset, then get the asset. 337 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 338 | assetRegistry.update(asset1).should.be.rejectedWith(/does not have .* access to resource/); 339 | 340 | }); 341 | 342 | it('Alice can remove her assets', async () => { 343 | // Use the identity for Alice. 344 | await useIdentity(aliceCardName); 345 | 346 | // Remove the asset, then test the asset exists. 347 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 348 | await assetRegistry.remove('1'); 349 | const exists = await assetRegistry.exists('1'); 350 | exists.should.be.false; 351 | }); 352 | 353 | it('Alice cannot remove Bob\'s assets', async () => { 354 | // Use the identity for Alice. 355 | await useIdentity(aliceCardName); 356 | 357 | // Remove the asset, then test the asset exists. 358 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 359 | assetRegistry.remove('2').should.be.rejectedWith(/does not have .* access to resource/); 360 | }); 361 | 362 | it('Bob can remove his assets', async () => { 363 | // Use the identity for Bob. 364 | await useIdentity(bobCardName); 365 | 366 | // Remove the asset, then test the asset exists. 367 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 368 | await assetRegistry.remove('2'); 369 | const exists = await assetRegistry.exists('2'); 370 | exists.should.be.false; 371 | }); 372 | 373 | it('Bob cannot remove Alice\'s assets', async () => { 374 | // Use the identity for Bob. 375 | await useIdentity(bobCardName); 376 | 377 | // Remove the asset, then test the asset exists. 378 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 379 | assetRegistry.remove('1').should.be.rejectedWith(/does not have .* access to resource/); 380 | }); 381 | 382 | it('Alice can submit a transaction for her assets', async () => { 383 | // Use the identity for Alice. 384 | await useIdentity(aliceCardName); 385 | 386 | // Submit the transaction. 387 | const transaction = factory.newTransaction(namespace, 'SampleTransaction'); 388 | transaction.asset = factory.newRelationship(namespace, assetType, '1'); 389 | transaction.newValue = '50'; 390 | await businessNetworkConnection.submitTransaction(transaction); 391 | 392 | // Get the asset. 393 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 394 | const asset1 = await assetRegistry.get('1'); 395 | 396 | // Validate the asset. 397 | asset1.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#alice@email.com'); 398 | asset1.value.should.equal('50'); 399 | 400 | // Validate the events. 401 | events.should.have.lengthOf(1); 402 | const event = events[0]; 403 | event.eventId.should.be.a('string'); 404 | event.timestamp.should.be.an.instanceOf(Date); 405 | event.asset.getFullyQualifiedIdentifier().should.equal(assetNS + '#1'); 406 | event.oldValue.should.equal('10'); 407 | event.newValue.should.equal('50'); 408 | }); 409 | 410 | it('Alice cannot submit a transaction for Bob\'s assets', async () => { 411 | // Use the identity for Alice. 412 | await useIdentity(aliceCardName); 413 | 414 | // Submit the transaction. 415 | const transaction = factory.newTransaction(namespace, 'SampleTransaction'); 416 | transaction.asset = factory.newRelationship(namespace, assetType, '2'); 417 | transaction.newValue = '50'; 418 | businessNetworkConnection.submitTransaction(transaction).should.be.rejectedWith(/does not have .* access to resource/); 419 | }); 420 | 421 | it('Bob can submit a transaction for his assets', async () => { 422 | // Use the identity for Bob. 423 | await useIdentity(bobCardName); 424 | 425 | // Submit the transaction. 426 | const transaction = factory.newTransaction(namespace, 'SampleTransaction'); 427 | transaction.asset = factory.newRelationship(namespace, assetType, '2'); 428 | transaction.newValue = '60'; 429 | await businessNetworkConnection.submitTransaction(transaction); 430 | 431 | // Get the asset. 432 | const assetRegistry = await businessNetworkConnection.getAssetRegistry(assetNS); 433 | const asset2 = await assetRegistry.get('2'); 434 | 435 | // Validate the asset. 436 | asset2.owner.getFullyQualifiedIdentifier().should.equal(participantNS + '#bob@email.com'); 437 | asset2.value.should.equal('60'); 438 | 439 | // Validate the events. 440 | events.should.have.lengthOf(1); 441 | const event = events[0]; 442 | event.eventId.should.be.a('string'); 443 | event.timestamp.should.be.an.instanceOf(Date); 444 | event.asset.getFullyQualifiedIdentifier().should.equal(assetNS + '#2'); 445 | event.oldValue.should.equal('20'); 446 | event.newValue.should.equal('60'); 447 | }); 448 | 449 | it('Bob cannot submit a transaction for Alice\'s assets', async () => { 450 | // Use the identity for Bob. 451 | await useIdentity(bobCardName); 452 | 453 | // Submit the transaction. 454 | const transaction = factory.newTransaction(namespace, 'SampleTransaction'); 455 | transaction.asset = factory.newRelationship(namespace, assetType, '1'); 456 | transaction.newValue = '60'; 457 | businessNetworkConnection.submitTransaction(transaction).should.be.rejectedWith(/does not have .* access to resource/); 458 | }); 459 | 460 | }); 461 | -------------------------------------------------------------------------------- /template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "namePrefix": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "String used as a base for naming resources (6 alphanumeric characters or less). A unique hash is prepended to the string for some resources, while resource-specific information is appended." 9 | }, 10 | "maxLength": 6 11 | }, 12 | "authType": { 13 | "type": "string", 14 | "allowedValues": [ 15 | "password", 16 | "sshPublicKey" 17 | ], 18 | "metadata": { 19 | "description": "Authorization type for SSH access to VMs" 20 | } 21 | }, 22 | "adminUsername": { 23 | "type": "string", 24 | "defaultValue": "azureuser", 25 | "metadata": { 26 | "description": "Administrator username of each deployed VM (alphanumeric characters only)" 27 | } 28 | }, 29 | "adminPassword": { 30 | "type": "securestring", 31 | "defaultValue": "", 32 | "metadata": { 33 | "description": "Administrator password for each deployed VM" 34 | } 35 | }, 36 | "adminSSHKey": { 37 | "type": "string", 38 | "defaultValue": "", 39 | "metadata": { 40 | "description": "SSH RSA public key file as a string" 41 | } 42 | }, 43 | "restrictAccess": { 44 | "type": "int", 45 | "defaultValue": 0, 46 | "metadata": { 47 | "description": "If 1, use specified IP address/subnet to restrict access to all endpoints. If 0, access is open to any IP address." 48 | } 49 | }, 50 | "ipAddressOrSubnet": { 51 | "type": "string", 52 | "defaultValue": "", 53 | "metadata": { 54 | "description": "If restrictAccess is set to 1, specify an individual IP address or IP subnet/address range here from where access to all endpoints will be allowed." 55 | } 56 | }, 57 | "numMembershipNodes": { 58 | "type": "int", 59 | "defaultValue": 1, 60 | "metadata": { 61 | "description": "Number of membership services nodes." 62 | }, 63 | "minValue": 1, 64 | "maxValue": 1 65 | }, 66 | "mnStorageAccountType": { 67 | "type": "string", 68 | "defaultValue": "Standard", 69 | "allowedValues": [ 70 | "Standard_LRS", 71 | "Standard_GRS", 72 | "Standard_RAGRS", 73 | "Premium_LRS" 74 | ], 75 | "metadata": { 76 | "description": "Storage performance level for Membership nodes" 77 | } 78 | }, 79 | "mnNodeVMSize": { 80 | "type": "string", 81 | "defaultValue": "Standard_D1_v2", 82 | "allowedValues": [], 83 | "metadata": { 84 | "description": "Size of the virtual machine used for Membership nodes" 85 | } 86 | }, 87 | "numOrdererNodes": { 88 | "type": "int", 89 | "defaultValue": 1, 90 | "metadata": { 91 | "description": "Number of orderer nodes." 92 | }, 93 | "minValue": 1, 94 | "maxValue": 1 95 | }, 96 | "onStorageAccountType": { 97 | "type": "string", 98 | "defaultValue": "Standard", 99 | "allowedValues": [ 100 | "Standard_LRS", 101 | "Standard_GRS", 102 | "Standard_RAGRS", 103 | "Premium_LRS" 104 | ], 105 | "metadata": { 106 | "description": "Storage performance level for Membership nodes" 107 | } 108 | }, 109 | "onNodeVMSize": { 110 | "type": "string", 111 | "defaultValue": "Standard_D1_v2", 112 | "allowedValues": [], 113 | "metadata": { 114 | "description": "Size of the virtual machine used for Membership nodes" 115 | } 116 | }, 117 | "numPeerNodes": { 118 | "type": "int", 119 | "defaultValue": 2, 120 | "metadata": { 121 | "description": "Number of peers." 122 | }, 123 | "minValue": 2, 124 | "maxValue": 9 125 | }, 126 | "pnStorageAccountType": { 127 | "type": "string", 128 | "defaultValue": "Standard_LRS", 129 | "allowedValues": [ 130 | "Standard_LRS", 131 | "Standard_GRS", 132 | "Standard_RAGRS", 133 | "Premium_LRS" 134 | ], 135 | "metadata": { 136 | "description": "Storage performance level for Membership nodes" 137 | } 138 | }, 139 | "pnNodeVMSize": { 140 | "type": "string", 141 | "defaultValue": "Standard_D1_v2", 142 | "allowedValues": [], 143 | "metadata": { 144 | "description": "Size of the virtual machine used for Membership nodes" 145 | } 146 | }, 147 | "CAUsername": { 148 | "type": "string", 149 | "defaultValue": "caadmin", 150 | "metadata": { 151 | "description": "Bootstrap user name for the Fabric CA" 152 | } 153 | }, 154 | "CAPassword": { 155 | "type": "securestring", 156 | "defaultValue": "", 157 | "metadata": { 158 | "description": "Bootstrap user password for the fabric CA" 159 | } 160 | }, 161 | "location": { 162 | "type": "string" 163 | }, 164 | "baseUrl": { 165 | "type": "string", 166 | "metadata": { 167 | "description": "The base URL for dependent assets", 168 | "artifactsBaseUrl": "" 169 | }, 170 | "defaultValue": "https://raw.githubusercontent.com/fabienpe/azure-hyperledger-artifacts/master" 171 | } 172 | }, 173 | "variables": { 174 | "apiVersionDeployments": "2016-09-01", 175 | "apiVersionPublicIPAddresses": "2016-09-01", 176 | "apiVersionAvailabilitySets": "2017-03-30", 177 | "apiVersionNetworkSecurityGroups": "2016-09-01", 178 | "apiVersionNetworkInterfaces": "2016-09-01", 179 | "apiVersionVirtualMachines": "2017-03-30", 180 | "apiVersionVirtualNetworks": "2016-09-01", 181 | "namingInfix": "[toLower(substring(concat(parameters('namePrefix'), uniqueString(resourceGroup().id)), 0, 9))]", 182 | "availabilitySetName": "[concat(variables('namingInfix'), 'fabricAvSet')]", 183 | "dnsName": "[variables('namingInfix')]", 184 | "publicIPAddressName": "[concat(variables('dnsName'), '-publicip')]", 185 | "loadBalancerName": "[concat(variables('namingInfix'), '-LB')]", 186 | "loadBalancerBackendAddressPoolName": "LBBackendPool", 187 | "loadBalancerInboundNatRuleNamePrefix": "NAT", 188 | "httpPort": 80, 189 | "sshPort": 22, 190 | "sshStartingPort": 3000, 191 | "adminSitePort": 3000, 192 | "caVMNamePrefix": "[concat(variables('namingInfix'), '-ca')]", 193 | "ordererVMNamePrefix": "[concat(variables('namingInfix'), '-orderer')]", 194 | "peerVMNamePrefix": "[concat(variables('namingInfix'), '-peer')]", 195 | "caNICPrefix": "nic-ca", 196 | "ordererNICPrefix": "nic-orderer", 197 | "peerNICPrefix": "nic-peer", 198 | "subnetName": "[uniqueString(concat(resourceGroup().id, concat(variables('namingInfix'), 'subnet')))]", 199 | "subnetPrefix": "10.0.0.0/24", 200 | "nsgName": "[concat(variables('namingInfix'), 'nsg')]", 201 | "sourceAddressPrefixArray": [ 202 | "*", 203 | "[parameters('ipAddressOrSubnet')]" 204 | ], 205 | "sourceAddressPrefix": "[variables('sourceAddressPrefixArray')[parameters('restrictAccess')]]", 206 | "subnetPropertiesArray": [ 207 | { 208 | "name": "[variables('subnetName')]", 209 | "properties": { 210 | "addressPrefix": "[variables('subnetPrefix')]", 211 | "networkSecurityGroup": { 212 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" 213 | } 214 | } 215 | } 216 | ], 217 | "ubuntuImage": { 218 | "publisher": "Canonical", 219 | "offer": "UbuntuServer", 220 | "sku": "16.04-LTS", 221 | "version": "16.04.201801120" 222 | }, 223 | "vNet": { 224 | "name": "[concat(variables('namingInfix'), 'vnet')]", 225 | "addressSpacePrefix": "10.0.0.0/20" 226 | }, 227 | "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('vNet').name)]", 228 | "subnetRef": "[concat(variables('vnetID'),'/subnets/', variables('subnetName'))]" 229 | }, 230 | "resources": [ 231 | { 232 | "apiVersion": "[variables('apiVersionAvailabilitySets')]", 233 | "type": "Microsoft.Compute/availabilitySets", 234 | "name": "[variables('availabilitySetName')]", 235 | "location": "[parameters('location')]", 236 | "sku": { 237 | "name": "Aligned" 238 | }, 239 | "properties": { 240 | "platformUpdateDomainCount": "2", 241 | "platformFaultDomainCount": "2" 242 | } 243 | }, 244 | { 245 | "apiVersion": "[variables('apiVersionPublicIPAddresses')]", 246 | "type": "Microsoft.Network/publicIPAddresses", 247 | "name": "[variables('publicIPAddressName')]", 248 | "location": "[parameters('location')]", 249 | "properties": { 250 | "publicIPAllocationMethod": "Dynamic", 251 | "dnsSettings": { 252 | "domainNameLabel": "[variables('dnsName')]" 253 | } 254 | } 255 | }, 256 | { 257 | "apiVersion": "[variables('apiVersionDeployments')]", 258 | "name": "loadBalancerLinkedTemplate", 259 | "type": "Microsoft.Resources/deployments", 260 | "dependsOn": [ 261 | "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIpAddressName'))]" 262 | ], 263 | "properties": { 264 | "mode": "incremental", 265 | "templateLink": { 266 | "uri": "[concat(parameters('baseUrl'), '/nested/loadBalancer.json')]", 267 | "contentVersion": "1.0.0.0" 268 | }, 269 | "parameters": { 270 | "loadBalancerName": { 271 | "value": "[variables('loadBalancerName')]" 272 | }, 273 | "dnsHostName": { 274 | "value": "[variables('namingInfix')]" 275 | }, 276 | "loadBalancerBackendAddressPoolName": { 277 | "value": "[variables('loadBalancerBackendAddressPoolName')]" 278 | }, 279 | "loadBalancerInboundNatRuleNamePrefix": { 280 | "value": "[variables('loadBalancerInboundNatRuleNamePrefix')]" 281 | }, 282 | "frontendPort1": { 283 | "value": "[variables('httpPort')]" 284 | }, 285 | "backendPort1": { 286 | "value": "[variables('adminSitePort')]" 287 | }, 288 | "numInboundNATRules": { 289 | "value": "[add(add(parameters('numOrdererNodes'), parameters('numMembershipNodes')), parameters('numPeerNodes'))]" 290 | }, 291 | "inboundNATRuleSSHStartingPort": { 292 | "value": "[variables('sshStartingPort')]" 293 | }, 294 | "inboundNATRuleSSHBackendPort": { 295 | "value": "[variables('sshPort')]" 296 | }, 297 | "publicIPAddressName": { 298 | "value": "[variables('publicIPAddressName')]" 299 | }, 300 | "location": { 301 | "value": "[parameters('location')]" 302 | } 303 | } 304 | } 305 | }, 306 | { 307 | "apiVersion": "[variables('apiVersionNetworkSecurityGroups')]", 308 | "type": "Microsoft.Network/networkSecurityGroups", 309 | "name": "[variables('nsgName')]", 310 | "location": "[parameters('location')]", 311 | "tags": { 312 | "displayName": "Network Security Group" 313 | }, 314 | "properties": { 315 | "securityRules": [ 316 | { 317 | "name": "allow-ssh", 318 | "properties": { 319 | "description": "Allow SSH", 320 | "protocol": "*", 321 | "sourcePortRange": "*", 322 | "destinationPortRange": "[variables('sshPort')]", 323 | "sourceAddressPrefix": "[variables('sourceAddressPrefix')]", 324 | "destinationAddressPrefix": "*", 325 | "access": "Allow", 326 | "priority": 100, 327 | "direction": "Inbound" 328 | } 329 | } 330 | ] 331 | } 332 | }, 333 | { 334 | "apiVersion": "[variables('apiVersionVirtualNetworks')]", 335 | "type": "Microsoft.Network/virtualNetworks", 336 | "name": "[variables('vNet').name]", 337 | "location": "[parameters('location')]", 338 | "dependsOn": [ 339 | "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]" 340 | ], 341 | "properties": { 342 | "addressSpace": { 343 | "addressPrefixes": [ 344 | "[variables('vNet').addressSpacePrefix]" 345 | ] 346 | }, 347 | "subnets": "[variables('subnetPropertiesArray')]" 348 | } 349 | }, 350 | { 351 | "apiVersion": "[variables('apiVersionDeployments')]", 352 | "name": "caVMLinkedTemplate", 353 | "type": "Microsoft.Resources/deployments", 354 | "dependsOn": [ 355 | "[concat('Microsoft.Network/virtualNetworks/', variables('vNet').name)]", 356 | "[concat('Microsoft.Compute/availabilitySets/', variables('availabilitySetName'))]", 357 | "loadBalancerLinkedTemplate" 358 | ], 359 | "properties": { 360 | "mode": "Incremental", 361 | "templateLink": { 362 | "uri": "[concat(parameters('baseUrl'), '/nested/VMAuth', '-', parameters('authType'), '.json')]", 363 | "contentVersion": "1.0.0.0" 364 | }, 365 | "parameters": { 366 | "apiVersionVirtualMachines": { 367 | "value": "[variables('apiVersionVirtualMachines')]" 368 | }, 369 | "apiVersionNetworkInterfaces": { 370 | "value": "[variables('apiVersionNetworkInterfaces')]" 371 | }, 372 | "storagePerformance": { 373 | "value": "[parameters('mnStorageAccountType')]" 374 | }, 375 | "loadBalancerName": { 376 | "value": "[variables('loadBalancerName')]" 377 | }, 378 | "loadBalancerBackendAddressPoolName": { 379 | "value": "[variables('loadBalancerBackendAddressPoolName')]" 380 | }, 381 | "loadBalancerInboundNatRuleNamePrefix": { 382 | "value": "[variables('loadBalancerInboundNatRuleNamePrefix')]" 383 | }, 384 | "subnetRef": { 385 | "value": "[variables('subnetRef')]" 386 | }, 387 | "vmNamePrefix": { 388 | "value": "[variables('caVMNamePrefix')]" 389 | }, 390 | "numVMs": { 391 | "value": "[parameters('numMembershipNodes')]" 392 | }, 393 | "offset": { 394 | "value": 0 395 | }, 396 | "nicPrefix": { 397 | "value": "[variables('caNICPrefix')]" 398 | }, 399 | "availabilitySetName": { 400 | "value": "[variables('availabilitySetName')]" 401 | }, 402 | "vmSize": { 403 | "value": "[parameters('mnNodeVMSize')]" 404 | }, 405 | "adminUsername": { 406 | "value": "[parameters('adminUsername')]" 407 | }, 408 | "adminPassword": { 409 | "value": "[parameters('adminPassword')]" 410 | }, 411 | "adminSSHKey": { 412 | "value": "[parameters('adminSSHKey')]" 413 | }, 414 | "ubuntuImage": { 415 | "value": "[variables('ubuntuImage')]" 416 | }, 417 | "namingInfix": { 418 | "value": "[variables('namingInfix')]" 419 | }, 420 | "location": { 421 | "value": "[parameters('location')]" 422 | } 423 | } 424 | } 425 | }, 426 | { 427 | "apiVersion": "[variables('apiVersionDeployments')]", 428 | "name": "ordererVMLinkedTemplate", 429 | "type": "Microsoft.Resources/deployments", 430 | "dependsOn": [ 431 | "[concat('Microsoft.Network/virtualNetworks/', variables('vNet').name)]", 432 | "[concat('Microsoft.Compute/availabilitySets/', variables('availabilitySetName'))]", 433 | "loadBalancerLinkedTemplate" 434 | ], 435 | "properties": { 436 | "mode": "Incremental", 437 | "templateLink": { 438 | "uri": "[concat(parameters('baseUrl'), '/nested/VMAuth', '-', parameters('authType'), '.json')]", 439 | "contentVersion": "1.0.0.0" 440 | }, 441 | "parameters": { 442 | "apiVersionVirtualMachines": { 443 | "value": "[variables('apiVersionVirtualMachines')]" 444 | }, 445 | "apiVersionNetworkInterfaces": { 446 | "value": "[variables('apiVersionNetworkInterfaces')]" 447 | }, 448 | "storagePerformance": { 449 | "value": "[parameters('onStorageAccountType')]" 450 | }, 451 | "loadBalancerName": { 452 | "value": "[variables('loadBalancerName')]" 453 | }, 454 | "loadBalancerBackendAddressPoolName": { 455 | "value": "[variables('loadBalancerBackendAddressPoolName')]" 456 | }, 457 | "loadBalancerInboundNatRuleNamePrefix": { 458 | "value": "[variables('loadBalancerInboundNatRuleNamePrefix')]" 459 | }, 460 | "subnetRef": { 461 | "value": "[variables('subnetRef')]" 462 | }, 463 | "vmNamePrefix": { 464 | "value": "[variables('ordererVMNamePrefix')]" 465 | }, 466 | "numVMs": { 467 | "value": "[parameters('numOrdererNodes')]" 468 | }, 469 | "offset": { 470 | "value": "[parameters('numMembershipNodes')]" 471 | }, 472 | "nicPrefix": { 473 | "value": "[variables('ordererNICPrefix')]" 474 | }, 475 | "availabilitySetName": { 476 | "value": "[variables('availabilitySetName')]" 477 | }, 478 | "vmSize": { 479 | "value": "[parameters('onNodeVMSize')]" 480 | }, 481 | "adminUsername": { 482 | "value": "[parameters('adminUsername')]" 483 | }, 484 | "adminPassword": { 485 | "value": "[parameters('adminPassword')]" 486 | }, 487 | "adminSSHKey": { 488 | "value": "[parameters('adminSSHKey')]" 489 | }, 490 | "ubuntuImage": { 491 | "value": "[variables('ubuntuImage')]" 492 | }, 493 | "namingInfix": { 494 | "value": "[variables('namingInfix')]" 495 | }, 496 | "location": { 497 | "value": "[parameters('location')]" 498 | } 499 | } 500 | } 501 | }, 502 | { 503 | "apiVersion": "[variables('apiVersionDeployments')]", 504 | "name": "peerVMLinkedTemplate", 505 | "type": "Microsoft.Resources/deployments", 506 | "dependsOn": [ 507 | "[concat('Microsoft.Network/virtualNetworks/', variables('vNet').name)]", 508 | "[concat('Microsoft.Compute/availabilitySets/', variables('availabilitySetName'))]", 509 | "loadBalancerLinkedTemplate" 510 | ], 511 | "properties": { 512 | "mode": "Incremental", 513 | "templateLink": { 514 | "uri": "[concat(parameters('baseUrl'), '/nested/VMAuth-', parameters('authType'), '.json')]", 515 | "contentVersion": "1.0.0.0" 516 | }, 517 | "parameters": { 518 | "apiVersionVirtualMachines": { 519 | "value": "[variables('apiVersionVirtualMachines')]" 520 | }, 521 | "apiVersionNetworkInterfaces": { 522 | "value": "[variables('apiVersionNetworkInterfaces')]" 523 | }, 524 | "storagePerformance": { 525 | "value": "[parameters('pnStorageAccountType')]" 526 | }, 527 | "loadBalancerName": { 528 | "value": "[variables('loadBalancerName')]" 529 | }, 530 | "loadBalancerBackendAddressPoolName": { 531 | "value": "[variables('loadBalancerBackendAddressPoolName')]" 532 | }, 533 | "loadBalancerInboundNatRuleNamePrefix": { 534 | "value": "[variables('loadBalancerInboundNatRuleNamePrefix')]" 535 | }, 536 | "subnetRef": { 537 | "value": "[variables('subnetRef')]" 538 | }, 539 | "vmNamePrefix": { 540 | "value": "[variables('peerVMNamePrefix')]" 541 | }, 542 | "numVMs": { 543 | "value": "[parameters('numPeerNodes')]" 544 | }, 545 | "offset": { 546 | "value": "[add(parameters('numMembershipNodes'), parameters('numOrdererNodes'))]" 547 | }, 548 | "nicPrefix": { 549 | "value": "[variables('peerNICPrefix')]" 550 | }, 551 | "availabilitySetName": { 552 | "value": "[variables('availabilitySetName')]" 553 | }, 554 | "vmSize": { 555 | "value": "[parameters('pnNodeVMSize')]" 556 | }, 557 | "adminUsername": { 558 | "value": "[parameters('adminUsername')]" 559 | }, 560 | "adminPassword": { 561 | "value": "[parameters('adminPassword')]" 562 | }, 563 | "adminSSHKey": { 564 | "value": "[parameters('adminSSHKey')]" 565 | }, 566 | "ubuntuImage": { 567 | "value": "[variables('ubuntuImage')]" 568 | }, 569 | "namingInfix": { 570 | "value": "[variables('namingInfix')]" 571 | }, 572 | "location": { 573 | "value": "[parameters('location')]" 574 | } 575 | } 576 | } 577 | }, 578 | { 579 | "apiVersion": "[variables('apiVersionDeployments')]", 580 | "name": "vmExtensionLinkedTemplate", 581 | "type": "Microsoft.Resources/deployments", 582 | "dependsOn": [ 583 | "caVMLinkedTemplate", 584 | "ordererVMLinkedTemplate", 585 | "peerVMLinkedTemplate" 586 | ], 587 | "properties": { 588 | "mode": "Incremental", 589 | "templateLink": { 590 | "uri": "[concat(parameters('baseUrl'), '/nested/VMExtension.json')]", 591 | "contentVersion": "1.0.0.0" 592 | }, 593 | "parameters": { 594 | "caVMNamePrefix": { 595 | "value": "[variables('caVMNamePrefix')]" 596 | }, 597 | "numMembershipNodes": { 598 | "value": "[parameters('numMembershipNodes')]" 599 | }, 600 | "ordererVMNamePrefix": { 601 | "value": "[variables('ordererVMNamePrefix')]" 602 | }, 603 | "numOrdererNodes": { 604 | "value": "[parameters('numOrdererNodes')]" 605 | }, 606 | "peerVMNamePrefix": { 607 | "value": "[variables('peerVMNamePrefix')]" 608 | }, 609 | "numPeerNodes": { 610 | "value": "[parameters('numPeerNodes')]" 611 | }, 612 | "adminUsername": { 613 | "value": "[parameters('adminUsername')]" 614 | }, 615 | "adminSitePort": { 616 | "value": "[variables('adminSitePort')]" 617 | }, 618 | "artifactsLocationURL": { 619 | "value": "[parameters('baseUrl')]" 620 | }, 621 | "CAUsername": { 622 | "value": "[parameters('CAUsername')]" 623 | }, 624 | "CAPassword": { 625 | "value": "[parameters('CAPassword')]" 626 | }, 627 | "namingInfix": { 628 | "value": "[variables('namingInfix')]" 629 | }, 630 | "location": { 631 | "value": "[parameters('location')]" 632 | } 633 | } 634 | } 635 | } 636 | ], 637 | "outputs": { 638 | "api-endpoint": { 639 | "type": "string", 640 | "value": "[concat('http://', reference(variables('publicIPAddressName')).dnsSettings.fqdn)]" 641 | }, 642 | "prefix": { 643 | "type": "string", 644 | "value": "[variables('namingInfix')]" 645 | }, 646 | "ssh-to-first-vm": { 647 | "type": "string", 648 | "value": "[concat('ssh -p ', variables('sshStartingPort'), ' ', parameters('adminUsername'), '@', reference(variables('publicIPAddressName')).dnsSettings.fqdn)]" 649 | } 650 | } 651 | } --------------------------------------------------------------------------------