Add support for multi-stack installation

Cleanup use of sudo for copying files. Apply docker specific
configuration inside user directory.

Utilize multiple juju controllers utilizing the stack name.

Create stack with no exposed host ports via '-nohostports' for creating
multiple instances of osm (useful in CI)

Change-Id: I32e2eab2cbae7fa6939eee2df556f6788d065a92
Signed-off-by: Mike Marchetti <mmarchetti@sandvine.com>
diff --git a/installers/docker/docker-compose.yaml b/installers/docker/docker-compose.yaml
index a1e88bc..fa27900 100644
--- a/installers/docker/docker-compose.yaml
+++ b/installers/docker/docker-compose.yaml
@@ -20,7 +20,7 @@
   kafka:
     image: wurstmeister/kafka
     ports:
-      - "9092:9092"
+      - "9092"
     networks:
       - netOSM
     environment:
@@ -49,7 +49,7 @@
       OSMNBI_DATABASE_HOST: mongo
       OSMNBI_MESSAGE_HOST: kafka
     ports:
-      - "9999:9999"
+      - "${OSM_NBI_PORTS:-9999:9999}"
     #depends_on:
     #  - kafka
     #  - mongo
@@ -90,7 +90,7 @@
     #depends_on:
     #  - ro-db
     ports:
-      - "9090:9090"
+      - "${OSM_RO_PORTS:-9090:9090}"
   mon:
     image: osm/mon
     networks:
@@ -105,7 +105,7 @@
     #depends_on:
     #  - kafka
     ports:
-      - "8662:8662"
+      - "8662"
   pm:
     image: osm/pm
     networks:
@@ -124,5 +124,4 @@
     #depends_on:
     #  - nbi
     ports:
-      - "80:80"
-
+      - "${OSM_UI_PORTS:-80:80}"
diff --git a/installers/full_install_osm.sh b/installers/full_install_osm.sh
index a92e1ba..60a99e3 100755
--- a/installers/full_install_osm.sh
+++ b/installers/full_install_osm.sh
@@ -26,6 +26,9 @@
     echo -e "                     -b v2.0            (v2.0 branch)"
     echo -e "                     -b tags/v1.1.0     (a specific tag)"
     echo -e "                     ..."
+    echo -e "     -s <stack name> user defined stack name, default is osm"
+    echo -e "     -H <VCA host>   use specific juju host controller IP"
+    echo -e "     -S <VCA secret> use VCA/juju secret key"
     echo -e "     --vimemu:       additionally deploy the VIM emulator as a docker container"
     echo -e "     --elk_stack:    additionally deploy an ELK docker stack for event logging"
     echo -e "     --pm_stack:     additionally deploy a Prometheus+Grafana stack for performance monitoring (PM)"
@@ -34,6 +37,9 @@
     echo -e "     -D <devops path> use local devops installation path"
     echo -e "     --nolxd:        do not install and configure LXD, allowing unattended installations (assumes LXD is already installed and confifured)"
     echo -e "     --nodocker:     do not install docker, do not initialize a swarm (assumes docker is already installed and a swarm has been initialized)"
+    echo -e "     --nojuju:       do not juju, assumes already installed"
+    echo -e "     --nodockerbuild:do not build docker images (use existing locally cached images)"
+    echo -e "     --nohostports:  do not expose docker ports to host (useful for creating multiple instances of osm on the same host)"
     echo -e "     --uninstall:    uninstall OSM: remove the containers and delete NAT rules"
     echo -e "     --source:       install OSM from source code using the latest stable tag"
     echo -e "     --develop:      (deprecated, use '-b master') install OSM from source code using the master branch"
@@ -72,6 +78,29 @@
     return 0
 }
 
+# takes a juju/accounts.yaml file and returns the password specific
+# for a controller. I wrote this using only bash tools to minimize 
+# additions of other packages
+function parse_juju_password {
+   password_file="${HOME}/.local/share/juju/accounts.yaml"
+   local controller_name=$1
+   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
+   sed -ne "s|^\($s\):|\1|" \
+        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
+        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $password_file |
+   awk -F$fs -v controller=$controller_name '{
+      indent = length($1)/2;
+      vname[indent] = $2;
+      for (i in vname) {if (i > indent) {delete vname[i]}}
+      if (length($3) > 0) {
+         vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
+         if (match(vn,controller) && match($2,"password")) {
+             printf("%s",$3);
+         }
+      }
+   }'
+}
+
 function remove_stack() {
     stack=$1
     if sg docker -c "docker stack ps ${stack}" ; then
@@ -102,17 +131,17 @@
         if [ -n "$INSTALL_ELK" ]; then
             echo -e "\nUninstalling OSM ELK stack"
             remove_stack osm_elk
-            sudo rm -rf /etc/osm/docker/osm_elk
+            rm -rf $OSM_DOCKER_WORK_DIR/osm_elk
         fi
         if [ -n "$INSTALL_PERFMON" ]; then
             echo -e "\nUninstalling OSM Performance Monitoring stack"
             remove_stack osm_metrics
             sg docker -c "docker image rm osm/kafka-exporter"
-            sudo rm -rf /etc/osm/docker/osm_metrics
+            rm -rf $OSM_DOCKER_WORK_DIR/osm_metrics
         fi
     else
         echo -e "\nUninstalling OSM"
-        remove_stack osm
+        remove_stack $OSM_STACK_NAME
         remove_stack osm_elk
         remove_stack osm_metrics
         echo "Now osm docker images and volumes will be deleted"
@@ -129,9 +158,8 @@
         docker volume rm osm_osm_packages
         docker volume rm osm_ro_db
 EONG
-        echo "Removing /etc/osm and /var/log/osm files"
-        sudo rm -rf /etc/osm
-        sudo rm -rf /var/log/osm
+        echo "Removing $OSM_DOCKER_WORK_DIR"
+        rm -rf $OSM_DOCKER_WORK_DIR
     fi
     echo "Some docker images will be kept in case they are used by other docker stacks"
     echo "To remove them, just run 'docker image prune' in a terminal"
@@ -579,9 +607,16 @@
     echo "Installing juju"
     sudo snap install juju --classic
     [ -z "$INSTALL_NOLXD" ] && sudo dpkg-reconfigure -p medium lxd
-    sg lxd -c "juju bootstrap --bootstrap-series=xenial localhost osm"
-    [ $(sg lxd -c "juju status" |grep "osm" |wc -l) -eq 1 ] || FATAL "Juju installation failed"
     echo "Finished installation of juju"
+    return 0
+}
+
+function juju_createcontroller() {
+    if ! sg lxd -c "juju show-controller $OSM_STACK_NAME &> /dev/null"; then
+        # Not found created, create the controller
+        sg lxd -c "juju bootstrap --bootstrap-series=xenial localhost $OSM_STACK_NAME"
+    fi
+    [ $(sg lxd -c "juju controllers" |grep "$OSM_STACK_NAME" |wc -l) -eq 1 ] || FATAL "Juju installation failed"
 }
 
 function generate_docker_images() {
@@ -633,34 +668,32 @@
     file2="$2"
     if ! $(cmp "${file1}" "${file2}" >/dev/null 2>&1); then
         if [ -f "${file2}" ]; then
-            ask_user "The file ${file2} already exists. Overwrite (y/N)? " n && sudo cp -b ${file1} ${file2}
+            ask_user "The file ${file2} already exists. Overwrite (y/N)? " n && cp -b ${file1} ${file2}
         else
-            sudo cp -b ${file1} ${file2}
+            cp -b ${file1} ${file2}
         fi
     fi
 }
 
 function generate_config_log_folders() {
     echo "Generating config and log folders"
-    sudo mkdir -p /etc/osm/docker
-    sudo cp -b ${OSM_DEVOPS}/installers/docker/docker-compose.yaml /etc/osm/docker/docker-compose.yaml
-    sudo mkdir -p /var/log/osm
+    cp -b ${OSM_DEVOPS}/installers/docker/docker-compose.yaml $OSM_DOCKER_WORK_DIR/docker-compose.yaml
     echo "Finished generation of config and log folders"
 }
 
 function generate_docker_env_files() {
     echo "Generating docker env files"
-    echo "OSMLCM_VCA_HOST=${OSMLCM_VCA_HOST}" |sudo tee /etc/osm/docker/lcm.env
-    echo "OSMLCM_VCA_SECRET=${OSMLCM_VCA_SECRET}" |sudo tee -a /etc/osm/docker/lcm.env
+    echo "OSMLCM_VCA_HOST=${OSMLCM_VCA_HOST}" | tee $OSM_DOCKER_WORK_DIR/lcm.env
+    echo "OSMLCM_VCA_SECRET=${OSMLCM_VCA_SECRET}" | tee -a $OSM_DOCKER_WORK_DIR/lcm.env
+
     MYSQL_ROOT_PASSWORD=`date +%s | sha256sum | base64 | head -c 32`
-    if [ ! -f /etc/osm/docker/ro-db.env ]; then
-        echo "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}" |sudo tee /etc/osm/docker/ro-db.env
+    if [ ! -f $OSM_DOCKER_WORK_DIR/ro-db.env ]; then
+        echo "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}" |tee $OSM_DOCKER_WORK_DIR/ro-db.env
     fi
-    if [ ! -f /etc/osm/docker/ro.env ]; then
-        echo "RO_DB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}" |sudo tee /etc/osm/docker/ro.env
+    if [ ! -f $OSM_DOCKER_WORK_DIR/ro.env ]; then
+        echo "RO_DB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}" |tee $OSM_DOCKER_WORK_DIR/ro.env
     fi
-    echo "OS_NOTIFIER_URI=http://${DEFAULT_IP}:8662" |tee /tmp/mon.env
-    cmp_overwrite /tmp/mon.env /etc/osm/docker/mon.env
+    echo "OS_NOTIFIER_URI=http://${DEFAULT_IP}:8662" |tee $OSM_DOCKER_WORK_DIR/mon.env
     echo "Finished generation of docker env files"
 }
 
@@ -678,9 +711,27 @@
 function deploy_lightweight() {
     echo "Deploying lightweight build"
     [ -n "$INSTALL_NODOCKER" ] || init_docker_swarm
-    remove_stack osm
-    sg docker -c "docker stack deploy -c /etc/osm/docker/docker-compose.yaml osm"
-    #docker-compose -f /etc/osm/docker/docker-compose.yaml up -d
+    remove_stack $OSM_STACK_NAME
+
+    OSM_NBI_PORT=9999
+    OSM_RO_PORT=9090
+    OSM_UI_PORT=80
+
+    if [ -n "$NO_HOST_PORTS" ]; then
+        OSM_PORTS+=(OSM_NBI_PORTS=$OSM_NBI_PORT)
+        OSM_PORTS+=(OSM_RO_PORTS=$OSM_RO_PORT)
+        OSM_PORTS+=(OSM_UI_PORTS=$OSM_UI_PORT)
+    else
+        OSM_PORTS+=(OSM_NBI_PORTS=$OSM_NBI_PORT:$OSM_NBI_PORT)
+        OSM_PORTS+=(OSM_RO_PORTS=$OSM_RO_PORT:$OSM_RO_PORT)
+        OSM_PORTS+=(OSM_UI_PORTS=$OSM_UI_PORT:$OSM_UI_PORT)
+    fi
+    echo "export ${OSM_PORTS[@]}" > $OSM_DOCKER_WORK_DIR/osm_ports.sh
+
+    pushd $OSM_DOCKER_WORK_DIR > /dev/null
+    sg docker -c "source ./osm_ports.sh; docker stack deploy -c $OSM_DOCKER_WORK_DIR/docker-compose.yaml $OSM_STACK_NAME"
+    popd > /dev/null
+
     echo "Finished deployment of lightweight build"
 }
 
@@ -690,11 +741,11 @@
     sg docker -c "docker pull docker.elastic.co/logstash/logstash-oss:6.2.3" || FATAL "cannot get logstash docker image"
     sg docker -c "docker pull docker.elastic.co/kibana/kibana-oss:6.2.3" || FATAL "cannot get kibana docker image"
     echo "Finished pulling elk docker images"
-    sudo mkdir -p /etc/osm/docker/osm_elk
-    sudo cp -b ${OSM_DEVOPS}/installers/docker/osm_elk/* /etc/osm/docker/osm_elk
+    mkdir -p "$OSM_DOCKER_WORK_DIR/osm_elk"
+    cp -b ${OSM_DEVOPS}/installers/docker/osm_elk/* $OSM_DOCKER_WORK_DIR/osm_elk
     remove_stack osm_elk
     echo "Deploying ELK stack"
-    sg docker -c "docker stack deploy -c /etc/osm/docker/osm_elk/docker-compose.yml osm_elk"
+    sg docker -c "docker stack deploy -c $OSM_DOCKER_WORK_DIR/osm_elk/docker-compose.yml osm_elk"
     echo "Waiting for ELK stack to be up and running"
     time=0
     step=5
@@ -740,17 +791,20 @@
     echo "Generating osm/kafka-exporter docker image"
     sg docker -c "docker build ${OSM_DEVOPS}/installers/docker/osm_metrics/kafka-exporter -f ${OSM_DEVOPS}/installers/docker/osm_metrics/kafka-exporter/Dockerfile -t osm/kafka-exporter --no-cache" || FATAL "cannot build kafka-exporter docker image"
     echo "Finished generation of osm/kafka-exporter docker image"
-    sudo mkdir -p /etc/osm/docker/osm_metrics
-    sudo cp -b ${OSM_DEVOPS}/installers/docker/osm_metrics/*.yml /etc/osm/docker/osm_metrics
-    sudo cp -b ${OSM_DEVOPS}/installers/docker/osm_metrics/*.json /etc/osm/docker/osm_metrics
+    mkdir -p $OSM_DOCKER_WORK_DIR/osm_metrics
+    cp -b ${OSM_DEVOPS}/installers/docker/osm_metrics/*.yml $OSM_DOCKER_WORK_DIR/osm_metrics
+    cp -b ${OSM_DEVOPS}/installers/docker/osm_metrics/*.json $OSM_DOCKER_WORK_DIR/osm_metrics
     remove_stack osm_metrics
     echo "Deploying PM stack (Kafka exporter + Prometheus + Grafana)"
-    sg docker -c "docker stack deploy -c /etc/osm/docker/osm_metrics/docker-compose.yml osm_metrics"
+    sg docker -c "docker stack deploy -c $OSM_DOCKER_WORK_DIR/osm_metrics/docker-compose.yml osm_metrics"
     echo "Finished deployment of PM stack"
     return 0
 }
 
 function install_lightweight() {
+    [ -z "$OSM_DOCKER_WORK_DIR" ] && OSM_DOCKER_WORK_DIR="$HOME/.osm/stack/$OSM_STACK_NAME"
+    [ ! -d "$OSM_DOCKER_WORK_DIR" ] && mkdir -p $OSM_DOCKER_WORK_DIR
+
     [ "$USER" == "root" ] && FATAL "You are running the installer as root. The installer is prepared to be executed as a normal user with sudo privileges."
     [ -z "$ASSUME_YES" ] && ! ask_user "The installation will configure LXD, install juju, install docker CE and init a docker swarm, as pre-requirements. Do you want to proceed (Y/n)? " y && echo "Cancelled!" && exit 1
     track proceed
@@ -762,7 +816,9 @@
     DEFAULT_IP=`ip -o -4 a |grep ${DEFAULT_IF}|awk '{split($4,a,"/"); print a[1]}'`
     [ -z "$DEFAULT_IP" ] && FATAL "Not possible to determine the IP address of the interface with the default route"
     DEFAULT_MTU=$(ip addr show ${DEFAULT_IF} | perl -ne 'if (/mtu\s(\d+)/) {print $1;}')
-    if [ -z "$INSTALL_NOLXD" ]; then
+
+    # if no host is passed in, we need to install lxd/juju, unless explicilty asked not to
+    if [ -z "$OSMLCM_VCA_HOST" ] && [ -z "$INSTALL_NOLXD" ]; then
         need_packages_lw="lxd"
         echo -e "Checking required packages: $need_packages_lw"
         dpkg -l $need_packages_lw &>/dev/null \
@@ -775,19 +831,26 @@
           || FATAL "failed to install $need_packages_lw"
     fi
     track prereqok
-    install_juju
-    OSMLCM_VCA_HOST=`sg lxd -c "juju show-controller"|grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
-    OSMLCM_VCA_SECRET=`grep password ${HOME}/.local/share/juju/accounts.yaml |awk '{print $2}'`
-    [ -z "$OSMLCM_VCA_HOST" ] && FATAL "Cannot obtain juju controller IP address"
-    [ -z "$OSMLCM_VCA_SECRET" ] && FATAL "Cannot obtain juju secret"
+    [ -z "$INSTALL_NOJUJU" ] && install_juju
+
+    if [ -z "$OSMLCM_VCA_HOST" ]; then
+        juju_createcontroller
+        OSMLCM_VCA_HOST=`sg lxd -c "juju show-controller $OSM_STACK_NAME"|grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
+        [ -z "$OSMLCM_VCA_HOST" ] && FATAL "Cannot obtain juju controller IP address"
+    fi
+    if [ -z "$OSMLCM_VCA_SECRET" ]; then
+        OSMLCM_VCA_SECRET=$(parse_juju_password $OSM_STACK_NAME)
+        [ -z "$OSMLCM_VCA_SECRET" ] && FATAL "Cannot obtain juju secret"
+    fi
+
     track juju
     [ -n "$INSTALL_NODOCKER" ] || install_docker_ce
     track docker_ce
     #install_docker_compose
-    generate_docker_images
+    [ -z "$DOCKER_NOBUILD" ] && generate_docker_images
     track docker_build
-    generate_config_log_folders
     generate_docker_env_files
+    generate_config_log_folders
     deploy_lightweight
     track docker_deploy
     [ -n "$INSTALL_VIMEMU" ] && install_vimemu && track vimemu
@@ -809,19 +872,20 @@
     git clone https://osm.etsi.org/gerrit/osm/vim-emu.git $EMUTEMPDIR
     # build vim-emu docker
     echo "Building vim-emu Docker container..."
-    sudo docker build -t vim-emu-img -f $EMUTEMPDIR/Dockerfile --no-cache $EMUTEMPDIR/ || FATAL "cannot build vim-emu-img docker image"
+
+    sg docker -c "docker build -t vim-emu-img -f $EMUTEMPDIR/Dockerfile --no-cache $EMUTEMPDIR/" || FATAL "cannot build vim-emu-img docker image"
     # start vim-emu container as daemon
     echo "Starting vim-emu Docker container 'vim-emu' ..."
     if [ -n "$INSTALL_LIGHTWEIGHT" ]; then
         # in lightweight mode, the emulator needs to be attached to netOSM
-        sudo docker run --name vim-emu -t -d --restart always --privileged --pid='host' --network=netOSM -v /var/run/docker.sock:/var/run/docker.sock vim-emu-img python examples/osm_default_daemon_topology_2_pop.py
+        sg docker -c "docker run --name vim-emu -t -d --restart always --privileged --pid='host' --network=netOSM -v /var/run/docker.sock:/var/run/docker.sock vim-emu-img python examples/osm_default_daemon_topology_2_pop.py"
     else
         # classic build mode
-        sudo docker run --name vim-emu -t -d --restart always --privileged --pid='host' -v /var/run/docker.sock:/var/run/docker.sock vim-emu-img python examples/osm_default_daemon_topology_2_pop.py
+        sg docker -c "docker run --name vim-emu -t -d --restart always --privileged --pid='host' -v /var/run/docker.sock:/var/run/docker.sock vim-emu-img python examples/osm_default_daemon_topology_2_pop.py"
     fi
     echo "Waiting for 'vim-emu' container to start ..."
     sleep 5
-    export VIMEMU_HOSTNAME=$(sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vim-emu)
+    export VIMEMU_HOSTNAME=$(sg docker -c "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vim-emu")
     echo "vim-emu running at ${VIMEMU_HOSTNAME} ..."
     # print vim-emu connection info
     echo -e "\nYou might be interested in adding the following vim-emu env variables to your .bashrc file:"
@@ -897,12 +961,18 @@
 TO_REBUILD=""
 INSTALL_NOLXD=""
 INSTALL_NODOCKER=""
+INSTALL_NOJUJU=""
 NOCONFIGURE=""
 RELEASE_DAILY=""
 SESSION_ID=`date +%s`
 OSM_DEVOPS=
+OSMLCM_VCA_HOST=
+OSMLCM_VCA_SECRET=
+OSM_STACK_NAME=osm
+NO_HOST_PORTS=""
+DOCKER_NOBUILD=""
 
-while getopts ":hy-:b:r:k:u:R:l:p:D:o:m:" o; do
+while getopts ":hy-:b:r:k:u:R:l:p:D:o:m:H:S:s:" o; do
     case "${o}" in
         h)
             usage && exit 0
@@ -931,6 +1001,15 @@
         D)
             OSM_DEVOPS="${OPTARG}"
             ;;
+        s)
+            OSM_STACK_NAME="${OPTARG}"
+            ;;
+        H)
+            OSMLCM_VCA_HOST="${OPTARG}"
+            ;;
+        S)
+            OSMLCM_VCA_SECRET="${OPTARG}"
+            ;;
         o)
             INSTALL_ONLY="y"
             [ "${OPTARG}" == "vimemu" ] && INSTALL_VIMEMU="y" && continue
@@ -968,6 +1047,9 @@
             [ "${OPTARG}" == "noconfigure" ] && NOCONFIGURE="y" && continue
             [ "${OPTARG}" == "showopts" ] && SHOWOPTS="y" && continue
             [ "${OPTARG}" == "daily" ] && RELEASE_DAILY="y" && continue
+            [ "${OPTARG}" == "nohostports" ] && NO_HOST_PORTS="y" && continue
+            [ "${OPTARG}" == "nojuju" ] && INSTALL_NOJUJU="y" && continue
+            [ "${OPTARG}" == "nodockerbuild" ] && DOCKER_NOBUILD="y" && continue
             echo -e "Invalid option: '--$OPTARG'\n" >&2
             usage && exit 1
             ;;