Pivotal Cloud Foundry on Azure - the Hard Way

Originally published at https://github.com/odedia/pcf-on-azure-the-hard-way

Below are installation instructions for PCF on Azure without any UI interaction (well… you might have to authenticate your azure account). For standard evaluation purposes, use the PCF on Azure marketplace for a one-click installation. If you want to geek out or get a handle on how things are working internally, read on.

Install Azure CLI from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest

Clears previous cache of logins if needed:

az account clear

az cloud set --name AzureCloud

Login

az login

Verify local cache of credentials az account list

Choose a location. I’ll use eastus in the examples below.

Create resource group:

LOCATION=eastus
az group create --name azure --location $LOCATION

Create jumpbox VM

az vm create   \
--resource-group azure   \
--name jumpbox   \
--image UbuntuLTS   \
--admin-username ubuntu   \
--data-disk-sizes-gb 200   \
--generate-ssh-keys   \
--vnet-address-prefix 192.168.0.0/16 \
--subnet-address-prefix 192.168.0.0/16 \
--private-ip-address 192.168.0.10  

Verify: az vm list

ssh ubuntu@`az vm list-ip-addresses \
               -n jumpbox \
               --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress \
               -o tsv`

Tip: if you want to connect from another machine, you will need to copy the ssh keys from the original machine to the new machine. The keys must be protected with 0600 unix permissions:

sudo chmod 600 ~/.ssh/id_rsa         
sudo chmod 600 ~/.ssh/id_rsa.pub    

Usually the key is under ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub but you can use other file names. For example, if the name of the key pair is azure and azure.pub you can connect with the following command:

ssh -i ~/.ssh/azure azureuser@`az vm list-ip-addresses \
               -n jumpbox \
               --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress \
               -o tsv`

From now on - all activites are done on the remote jumpbox.

Install azure cli and other tools on remote jumpbox:

sudo apt-get install apt-transport-https lsb-release software-properties-common -y
AZ_REPO=$(lsb_release -cs)
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \
    sudo tee /etc/apt/sources.list.d/azure-cli.list

sudo apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv \
     --keyserver packages.microsoft.com \
     --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF

sudo apt-get update

sudo apt-get install azure-cli && sudo apt --yes install unzip && sudo apt --yes install jq

wget -O terraform.zip https://releases.hashicorp.com/terraform/0.11.8/terraform_0.11.8_linux_amd64.zip && \
  unzip terraform.zip && \
  sudo mv terraform /usr/local/bin

wget -O om https://github.com/pivotal-cf/om/releases/download/0.41.0/om-linux && \
  chmod +x om && \
  sudo mv om /usr/local/bin/

wget -O bosh https://s3.amazonaws.com/bosh-cli-artifacts/bosh-cli-5.3.1-linux-amd64 && \
  chmod +x bosh && \
  sudo mv bosh /usr/local/bin/

wget -O /tmp/bbr.tar https://github.com/cloudfoundry-incubator/bosh-backup-and-restore/releases/download/v1.2.8/bbr-1.2.8.tar && \
  tar xvC /tmp/ -f /tmp/bbr.tar && \
  sudo mv /tmp/releases/bbr /usr/local/bin/
  

az login

Create .env file:


PCF_PIVNET_UAA_TOKEN=<redacted>   # see https://network.pivotal.io/users/dashboard/edit-profile
PCF_DOMAIN_NAME=<redacted>        # buy one for cheap on domains.google (~10$ a year)
PCF_SUBDOMAIN_NAME=<redacted>     # az group list | jq -r .[0].name
PCF_OPSMAN_ADMIN_PASSWD=<choose secure admin password>

PCF_OPSMAN_FQDN=pcf.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}
USER_ID=<user> #change to match your user (odedia for example), this is for unique service account name creation
CLIENT_SECRET=<choose secure password>
DECRYPT_PHRASE=<choose secure key and save it safely! Your env is dead without this key!>
CREDHUB_KEY=<choose a secure key over 21 characters long>
NOTIFICATIONS_EMAIL=<your email>

Source the .env file and add to the env of .bashrc:

source ~/.env
echo "source ~/.env" >> ~/.bashrc

Create an Azure Active Directory application (AAD) for BOSH:

az ad app create \
--display-name "Service Principal for BOSH" \
--password $CLIENT_SECRET \
--homepage "http://BOSHAzureCPI" \
--identifier-uris "http://${USER_ID}BOSHAzureCPI"

(Tip: if you ever need to delete the AAD use this: az ad app delete --id "http://${USER_ID}BOSHAzureCPI")

Create a service principal from the AAD:

az ad sp create --id `az ad app show --id http://${USER_ID}BOSHAzureCPI | jq -r .appId`

Add the contributor role:

az role assignment create --assignee http://${USER_ID}BOSHAzureCPI --role "Contributor" --scope /subscriptions/`az account list | jq -r .[0].id`

Verify role assignment with this command:

az role assignment list --assignee "http://${USER_ID}BOSHAzureCPI"

Make sure the account is valid by logging in to the AAD:

az login \
--username `az ad app show --id http://${USER_ID}BOSHAzureCPI | jq -r .appId` \
--password $CLIENT_SECRET \
--service-principal \
--tenant `az account list | jq -r .[0].tenantId`

Once confirmed, enable compute, netowrk and storage access:

az provider register --namespace Microsoft.Storage
az provider register --namespace Microsoft.Network
az provider register --namespace Microsoft.Compute

logout and login again to your regular azure account:

az logout
az login

Create a self-signed certificate for installation:

cat > ./${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.cnf <<-EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C=IL
ST=Israel
L=Tel Aviv
O=Oded Shopen
OU=DEMO
CN = ${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = *.sys.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}
DNS.2 = *.login.sys.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}
DNS.3 = *.uaa.sys.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}
DNS.4 = *.apps.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}
EOF

Generate the key:

openssl req -x509 \
  -newkey rsa:2048 \
  -nodes \
  -keyout ${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.key \
  -out ${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.cert \
  -config ./${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.cnf

Set variables for PAS installation from pivotal network. For example, using this URL https://network.pivotal.io/products/elastic-runtime/#/releases/220833 we can interpolate the following:

PRODUCT_SLUG="elastic-runtime"
RELEASE_ID="latest"

Authenticate with Pivnet:

AUTHENTICATION_RESPONSE=$(curl \
  --fail \
  --data "{\"refresh_token\": \"${PCF_PIVNET_UAA_TOKEN}\"}" \
  https://network.pivotal.io/api/v2/authentication/access_tokens)

Get the access token:

PIVNET_ACCESS_TOKEN=$(echo ${AUTHENTICATION_RESPONSE} | jq -r '.access_token')

Get the release JSON for the PAS version you want to install:

  RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/${PRODUCT_SLUG}/releases/${RELEASE_ID}")

Accept EULA:

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

Extract the terraform download URL for azure:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains("terraforming-azure"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

Download and unzip:

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}
unzip ./${FILENAME}
cd ./pivotal-cf-terraforming-azure-*/
cd terraforming-pas

Create tfvars file:

touch terraform.tfvars

Edit the terraform file with the following parameters:

echo subscription_id       = \"`az account list | jq -r .[0].id`\" >> terraform.tfvars
echo tenant_id             = \"`az account list | jq -r .[0].tenantId`\" >> terraform.tfvars
echo client_id             = \"`az ad app show --id http://${USER_ID}BOSHAzureCPI | jq -r .appId`\" >> terraform.tfvars
echo client_secret         = \"${CLIENT_SECRET}\"  >> terraform.tfvars

echo env_name              = \"${PCF_SUBDOMAIN_NAME}\"   >> terraform.tfvars
echo location              = \"East US\" >> terraform.tfvars

RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/ops-manager/releases/latest")

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}


DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains("onAzure.yml"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

echo ops_manager_image_uri = \"`cat *onAzure.yml | grep east_us | sed 's/east_us: //'`\" >> terraform.tfvars
rm *onAzure.yml

echo dns_suffix            = \"${PCF_DOMAIN_NAME}\" >> terraform.tfvars
echo vm_admin_username     = \"admin\" >> terraform.tfvars
echo isolation_segment 	  = \"true\" >> terraform.tfvars
echo env_short_name = \"pcf${USER_ID}\" >> terraform.tfvars

Inspect terraform.tfvars to confirm the settings look ok.

Init terraform:

terraform init
terraform plan -out=plan
terraform apply --auto-approve

When terraform installation is complete, setup the DNS records in your google domain provider as type NS. Look at the terraform output env_dns_zone_name_servers. For example:

env_dns_zone_name_servers = [
    ns2-03.azure-dns.net.,
    ns3-03.azure-dns.org.,
    ns1-03.azure-dns.com.,
    ns4-03.azure-dns.info.
]

Set these records in google domains. The name should be your PCF_SUBDOMAIN_NAME. The type should be NS. the nameservers are taken from env_dns_zone_name_servers.

You will need to wait until the changes are propegated to your DNS provider. Use the following command to flush your local DNS cache (on macOS):

sudo killall -HUP mDNSResponder

Use the following command to query if your DNS entry is propegated:

nslookup
>set q=NS
>subdomain.domain.dom (change to your values to match $PCF_SUBDOMAIN_NAME.$PCF_DOMAIN_NAME)

Once there is a proper response for the above command, you can continue.

Configure opsman authentication using the om cli:

om \
  --target https://$PCF_OPSMAN_FQDN \
  --skip-ssl-validation \
    configure-authentication \
      --username admin \
      --password $PCF_OPSMAN_ADMIN_PASSWD \
      --decryption-passphrase $DECRYPT_PHRASE

You can go to the value of $PCF_OPSMAN_FQDN in your browser to see Ops Manager but we’ll try to do everything from the command line.

Set the properties under BOSH director with the following loooooooooooooong command:


om --target https://$PCF_OPSMAN_FQDN --skip-ssl-validation --username admin --password $PCF_OPSMAN_ADMIN_PASSWD \
    configure-director \
      --director-configuration '{
        "ntp_servers_string": "us.pool.ntp.org",
        "resurrector_enabled": "true"
      }' \
      --iaas-configuration '{
        "subscription_id": "'"`terraform output subscription_id`"'",
        "tenant_id": "'"`terraform output tenant_id`"'",
        "client_id": "'"`terraform output client_id`"'",
        "client_secret": "'"${CLIENT_SECRET}"'",
        "resource_group_name": "'"`terraform output pcf_resource_group_name`"'",
        "bosh_storage_account_name": "'"`terraform output bosh_root_storage_account`"'",
        "ssh_public_key": "'"`terraform output ops_manager_ssh_public_key`"'",
        "ssh_private_key": '"`terraform output -json ops_manager_ssh_private_key | jq .value`"'
      }' \
      --networks-configuration "{
        \"icmp_checks_enabled\": false,
        \"networks\": [
          {
            \"name\": \"Management\",
            \"subnets\": [
              {
                \"iaas_identifier\": "\""`terraform output network_name`/`terraform output management_subnet_name`"\"",
                \"cidr\": "\""`terraform output management_subnet_cidrs`"\"",
                \"reserved_ip_ranges\": "\""`terraform output management_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1"-"$1,$2,$3,$4+9}'`"\"",
                \"dns\": \"168.63.129.16\",
                \"gateway\": "\""`terraform output management_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1}'`"\""
              }
            ]
          },
          {
            \"name\": \"PAS\",
            \"subnets\": [
              {
                \"iaas_identifier\": "\""`terraform output network_name`/`terraform output pas_subnet_name`"\"",
                \"cidr\": "\""`terraform output pas_subnet_cidrs`"\"",
                \"reserved_ip_ranges\": "\""`terraform output pas_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1"-"$1,$2,$3,$4+9}'`"\"",
                \"dns\": \"168.63.129.16\",
                \"gateway\": "\""`terraform output pas_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1}'`"\""
              }
            ]
          },
          {
            \"name\": \"Services\",
            \"service_network\": true,
            \"subnets\": [
              {
                \"iaas_identifier\": "\""`terraform output network_name`/`terraform output services_subnet_name`"\"",
                \"cidr\": "\""`terraform output services_subnet_cidrs`"\"",
                \"reserved_ip_ranges\": "\""`terraform output services_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1"-"$1,$2,$3,$4+9}'`"\"",
                \"dns\": \"168.63.129.16\",
                \"gateway\": "\""`terraform output services_subnet_cidrs | awk -F . 'BEGIN {OFS="."} {print $1,$2,$3,$4+1}'`"\""
              }            
            ]
      }

    ]  
}" \
--network-assignment '{
  "network": {
    "name" : "Management"
  }
}' \
--resource-configuration '{
  "compilation" : {
    "instances": 8
  }
}'

You might want to review the changes in a browser (go to $PCF_OPSMAN_FQDN).

Apply changes. This will take about 10 minutes:

om --target https://$PCF_OPSMAN_FQDN --skip-ssl-validation --username admin --password $PCF_OPSMAN_ADMIN_PASSWD apply-changes

Create network peering from the jumpbox to the PCF network:

az network vnet peering create --name jumpbox-peering --remote-vnet azure-virtual-network --resource-group azure --vnet-name jumpboxVNET --allow-forwarded-traffic --allow-gateway-transit --allow-vnet-access

az network vnet peering create --name opsman-peering --remote-vnet jumpboxVNET --resource-group azure --vnet-name azure-virtual-network --allow-forwarded-traffic --allow-gateway-transit --allow-vnet-access


Generate the BOSH environment variables: (Put BOSH environment variables in the ~/.env file so you don’t have to run it again if you get disconnected)

echo "export $( \
  om \
    --skip-ssl-validation \
    --target ${PCF_OPSMAN_FQDN} \
    --username admin \
    --password ${PCF_OPSMAN_ADMIN_PASSWD} \
    curl \
      --silent \
      --path /api/v0/deployed/director/credentials/bosh_commandline_credentials | \
        jq --raw-output '.credential' \
)" >> ~/.env

source ~/.env

Copy the root certificate to the jumpbox:

sudo mkdir -p /var/tempest/workspaces/default

sudo sh -c \
  "om \
    --skip-ssl-validation \
    --target ${PCF_OPSMAN_FQDN} \
    --username admin \
    --password ${PCF_OPSMAN_ADMIN_PASSWD} \
    curl \
      --silent \
      --path "/api/v0/security/root_ca_certificate" |
        jq --raw-output '.root_ca_certificate_pem' \
          > /var/tempest/workspaces/default/root_ca_certificate"

Verify connectivity:

bosh env

Check the BOSH tasks that were executed so far. Not many, yet.

bosh tasks --recent=30

Inspect the single task that was executed:

bosh task 1

Inspect the single task in debug mode:

bosh task 1 --debug

Installing PAS

Authenticate:

AUTHENTICATION_RESPONSE=$(curl \
  --fail \
  --data "{\"refresh_token\": \"${PCF_PIVNET_UAA_TOKEN}\"}" \
  https://network.pivotal.io/api/v2/authentication/access_tokens)

Get Token:

PIVNET_ACCESS_TOKEN=$(echo ${AUTHENTICATION_RESPONSE} | jq -r '.access_token')

Get the latest PAS:

cd ~

RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/elastic-runtime/releases/latest")

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

For the Small Footprint installation, use the following:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains("elastic-runtime/srt"))')

For the full PAS, use the following:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains("elastic-runtime/cf-2"))')

Extract the download URL:

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

Download the file:

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

Upload the tile:

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  upload-product \
    --product ${FILENAME}

Stage the tile:

PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  available-products \
    --format json)

VERSION=$(echo ${PRODUCTS} |\
  jq -r 'map(select(.name == "'cf'")) | first | .version')

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  stage-product \
    --product-name "cf" \
    --product-version ${VERSION}

Get Staged product GUID:

STAGED_PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products)

PRODUCT_GUID=$(echo ${STAGED_PRODUCTS} |\
  jq -r 'map(select(.type == "'cf'")) | first | .guid')

Find configurable properties:

PROPERTIES=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products/${PRODUCT_GUID}/properties)
    

Setup network settings:

NETWORK_SETTINGS_JSON=$(cat <<-EOF
{
  "singleton_availability_zone": {
    "name": "null"
  },
  "other_availability_zones": [
    {
      "name": "null"
    }
  ],
  "network": {
    "name": "PAS"
  }
}
EOF
)

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  configure-product \
    --product-name "cf" \
    --product-network "${NETWORK_SETTINGS_JSON}"

Setup other properties:

CERT_PEM=$(cat ~/${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.cert | awk '{printf "%s\\r\\n", $0}')
KEY_PEM=$(cat ~/${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}.key | awk '{printf "%s\\r\\n", $0}')


PROPERTIES_JSON=$(cat <<-EOF
  {
   ".cloud_controller.system_domain": {
     "value": "sys.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}"
   },
   ".cloud_controller.apps_domain": {
     "value": "apps.${PCF_SUBDOMAIN_NAME}.${PCF_DOMAIN_NAME}"
   },
   ".properties.haproxy_forward_tls": {
     "value": "disable"
   },
   ".ha_proxy.skip_cert_verify": {
      "value": true
   },
   ".properties.security_acknowledgement": {
      "value": "X"
   },
   ".uaa.service_provider_key_credentials": {
      "value": {
        "private_key_pem": "${KEY_PEM}",
        "cert_pem": "${CERT_PEM}"
      }
   },
   ".properties.networking_poe_ssl_certs": {
      "value": [
        {
          "name": "default",
          "certificate": {
              "private_key_pem": "${KEY_PEM}",
              "cert_pem": "${CERT_PEM}"
          }
        }
      ]
    },
    ".properties.credhub_key_encryption_passwords": {
      "value": [
        {
          "name": "default",
          "provider": "internal",
          "key": {
            "secret": "${CREDHUB_KEY}"
          },
          "primary": true
        }
      ] 
    },
    ".mysql_monitor.recipient_email": {
      "value": "${NOTIFICATIONS_EMAIL}"
    }
  }
EOF
)

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  configure-product \
    --product-name "cf" \
    --product-properties "${PROPERTIES_JSON}"

Configure the resource jobs:

cd ./pivotal-cf-terraforming-azure-*/
cd terraforming-pas

JOBS_PROPERTIES=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products/${PRODUCT_GUID}/jobs)
    
WEB_LB=`terraform output web_lb_name`   
JOB_GUID=`echo $JOBS_PROPERTIES | jq -r '.jobs[] | select(.name =="router")|.guid'`


om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products/${PRODUCT_GUID}/jobs/${JOB_GUID}/resource_config \
    -x PUT -d '{
          "instances": 1,
          "instance_type": {
            "id": "automatic"
          },
          "elb_names": ['"${WEB_LB}"']
        }'


TCP_LB=`terraform output tcp_lb_name`
JOB_GUID=`echo $JOBS_PROPERTIES | jq -r '.jobs[] | select(.name =="tcp_router")|.guid'`

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products/${PRODUCT_GUID}/jobs/${JOB_GUID}/resource_config \
    -x PUT -d '{
          "instances": 1,
          "persistent_disk": {
            "size_mb": "automatic"
          },
          "instance_type": {
            "id": "automatic"
          },
          "elb_names": ["'"${TCP_LB}"'"]
        }'
        
        
DIEGO_LB=`terraform output diego_ssh_lb_name`
JOB_GUID=`echo $JOBS_PROPERTIES | jq -r '.jobs[] | select(.name =="control")|.guid'`

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products/${PRODUCT_GUID}/jobs/${JOB_GUID}/resource_config \
    -x PUT -d '{
          "instances": 1,
          "instance_type": {
            "id": "automatic"
          },
          "elb_names": ["'"${DIEGO_LB}"'"]
        }'
        
om --target https://$PCF_OPSMAN_FQDN --skip-ssl-validation --username admin --password $PCF_OPSMAN_ADMIN_PASSWD apply-changes

You can CTRL-C (break) the execution in the middle, the installation will continue. You can check the progress in https://$PCF_OPSMAN_FQDN

Inspect bosh tasks while installation is running:

bosh tasks --recent=30

Inspect the single task that was executed:

bosh task 20

Some tasks might output information about the Cloud Provider Interface (CPI):

bosh task 20 --cpi

Installing Stemcell for PAS

PRODUCT_SLUG="stemcells-ubuntu-xenial"
RELEASE_ID="latest"

Authenticate with Pivnet:

AUTHENTICATION_RESPONSE=$(curl \
  --fail \
  --data "{\"refresh_token\": \"${PCF_PIVNET_UAA_TOKEN}\"}" \
  https://network.pivotal.io/api/v2/authentication/access_tokens)

Get the access token:

PIVNET_ACCESS_TOKEN=$(echo ${AUTHENTICATION_RESPONSE} | jq -r '.access_token')

Get the release JSON for the PAS version you want to install:

  RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/${PRODUCT_SLUG}/releases/${RELEASE_ID}")

Accept EULA:

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

Extract the download URL:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains("azure-hyperv-ubuntu-xenial-go_agent.tgz"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

Upload the stemcell:

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  upload-stemcell \
    --stemcell ${FILENAME}

You now have a working PCF environment! You can continue below for reference but please consider it an incomplete example.

Installing MySQL

PRODUCT_SLUG="pivotal-mysql"
RELEASE_ID="latest"

Authenticate with Pivnet:

AUTHENTICATION_RESPONSE=$(curl \
  --fail \
  --data "{\"refresh_token\": \"${PCF_PIVNET_UAA_TOKEN}\"}" \
  https://network.pivotal.io/api/v2/authentication/access_tokens)

Get the access token:

PIVNET_ACCESS_TOKEN=$(echo ${AUTHENTICATION_RESPONSE} | jq -r '.access_token')

Get the release JSON for the PAS version you want to install:

  RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/${PRODUCT_SLUG}/releases/${RELEASE_ID}")

Accept EULA:

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

Extract the download URL:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains(".pivotal"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

Upload the tile:

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  upload-product \
    --product ${FILENAME}

Stage the tile:

PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  available-products \
    --format json)

VERSION=$(echo ${PRODUCTS} |\
  jq -r 'map(select(.name == "'pivotal-mysql'")) | first | .version')

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  stage-product \
    --product-name "pivotal-mysql" \
    --product-version ${VERSION}

Get Staged product GUID:

STAGED_PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products)

PRODUCT_GUID=$(echo ${STAGED_PRODUCTS} |\
  jq -r 'map(select(.type == "'pivotal-mysql'")) | first | .guid')

Installing Redis

Get the release JSON for the PAS version you want to install:

  RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/p-redis/releases/latest")

Accept EULA:

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

Extract the download URL:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains(".pivotal"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

Upload the tile:

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  upload-product \
    --product ${FILENAME}

Stage the tile:

PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  available-products \
    --format json)

VERSION=$(echo ${PRODUCTS} |\
  jq -r 'map(select(.name == "'p-redis'")) | first | .version')

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  stage-product \
    --product-name "p-redis" \
    --product-version ${VERSION}

Get Staged product GUID:

STAGED_PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products)

PRODUCT_GUID=$(echo ${STAGED_PRODUCTS} |\
  jq -r 'map(select(.type == "'p-healthwatch'")) | first | .guid')

Installing Healthwatch

Get the release JSON for the PAS version you want to install:

  RELEASE_JSON=$(curl \
    --fail \
    "https://network.pivotal.io/api/v2/products/p-healthwatch/releases/latest")

Accept EULA:

EULA_ACCEPTANCE_URL=$(echo ${RELEASE_JSON} |\
  jq -r '._links.eula_acceptance.href')

curl \
  --fail \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  --request POST \
  ${EULA_ACCEPTANCE_URL}

Extract the download URL:

DOWNLOAD_ELEMENT=$(echo ${RELEASE_JSON} |\
  jq -r '.product_files[] | select(.aws_object_key | contains(".pivotal"))')

FILENAME=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '.aws_object_key | split("/") | last')

URL=$(echo ${DOWNLOAD_ELEMENT} |\
  jq -r '._links.download.href')

curl \
  --fail \
  --location \
  --output ${FILENAME} \
  --header "Authorization: Bearer ${PIVNET_ACCESS_TOKEN}" \
  ${URL}

Upload the tile:

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  upload-product \
    --product ${FILENAME}

Stage the tile:

PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  available-products \
    --format json)

VERSION=$(echo ${PRODUCTS} |\
  jq -r 'map(select(.name == "'p-healthwatch'")) | first | .version')

om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  stage-product \
    --product-name "p-healthwatch" \
    --product-version ${VERSION}

Get Staged product GUID:

STAGED_PRODUCTS=$(om \
  --username admin \
  --password ${PCF_OPSMAN_ADMIN_PASSWD} \
  --target ${PCF_OPSMAN_FQDN} \
  --skip-ssl-validation \
  curl \
    --path /api/v0/staged/products)

PRODUCT_GUID=$(echo ${STAGED_PRODUCTS} |\
  jq -r 'map(select(.type == "'p-healthwatch'")) | first | .guid')

Acknoledgements: Some commands taken from https://github.com/amcginlay/bosh-topics. Much thanks to Alan McGinlay for training and guidance! Additional pull requests from Karsten Bott https://github.com/bottkars