summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tools/laas-fog/LaaS_Diagram.jpgbin0 -> 888779 bytes
-rw-r--r--tools/laas-fog/README167
-rw-r--r--tools/laas-fog/conf/domain.yaml108
-rw-r--r--tools/laas-fog/conf/fuel.yaml17
-rw-r--r--tools/laas-fog/conf/inventory.yaml6
-rw-r--r--tools/laas-fog/conf/joid.yaml17
-rw-r--r--tools/laas-fog/conf/laas.yaml17
-rw-r--r--tools/laas-fog/conf/network.yaml52
-rw-r--r--tools/laas-fog/conf/pharos.yaml11
-rw-r--r--tools/laas-fog/conf/vpn.yaml15
-rwxr-xr-xtools/laas-fog/hostScripts/fuelInstall.sh40
-rwxr-xr-xtools/laas-fog/hostScripts/horizonNat.sh31
-rwxr-xr-xtools/laas-fog/hostScripts/ipnat.sh34
-rwxr-xr-xtools/laas-fog/hostScripts/joidInstall.sh33
-rwxr-xr-xtools/laas-fog/hostScripts/mkDisks.sh20
-rwxr-xr-xtools/laas-fog/hostScripts/vncAllow.sh23
-rw-r--r--tools/laas-fog/source/__init__.py17
-rw-r--r--tools/laas-fog/source/api/__init__.py17
-rw-r--r--tools/laas-fog/source/api/fog.py288
-rw-r--r--tools/laas-fog/source/api/fuel_api.py306
-rw-r--r--tools/laas-fog/source/api/libvirt_api.py331
-rw-r--r--tools/laas-fog/source/api/vpn.py235
-rw-r--r--tools/laas-fog/source/database.py296
-rwxr-xr-xtools/laas-fog/source/deploy.py82
-rw-r--r--tools/laas-fog/source/deployment_manager.py108
-rw-r--r--tools/laas-fog/source/domain.py244
-rw-r--r--tools/laas-fog/source/installers/__init__.py17
-rw-r--r--tools/laas-fog/source/installers/fuel.py268
-rw-r--r--tools/laas-fog/source/installers/installer.py35
-rw-r--r--tools/laas-fog/source/installers/joid.py40
-rwxr-xr-xtools/laas-fog/source/listen.py59
-rw-r--r--tools/laas-fog/source/network.py103
-rwxr-xr-xtools/laas-fog/source/pharos.py217
-rwxr-xr-xtools/laas-fog/source/pod_manager.py144
-rwxr-xr-xtools/laas-fog/source/resetDataBase.py110
-rwxr-xr-xtools/laas-fog/source/stop.sh24
-rw-r--r--tools/laas-fog/source/utilities.py346
37 files changed, 3878 insertions, 0 deletions
diff --git a/tools/laas-fog/LaaS_Diagram.jpg b/tools/laas-fog/LaaS_Diagram.jpg
new file mode 100644
index 00000000..521236d1
--- /dev/null
+++ b/tools/laas-fog/LaaS_Diagram.jpg
Binary files differ
diff --git a/tools/laas-fog/README b/tools/laas-fog/README
new file mode 100644
index 00000000..84317eb3
--- /dev/null
+++ b/tools/laas-fog/README
@@ -0,0 +1,167 @@
+This Lab as a Serice project aims to create on demand OPNFV resources to developers.
+This project will automate the process, to the requested extent, of running an OPNFV
+installer and creating an Openstack environment within OPNFV automatically and on demand.
+
+To run, execute (from the project root):
+ source/deploy.py
+
+To run the Pharos dahsboard listener, which will continualy poll the dashboard and run deployments in the background:
+ source/listen.py --config <conf/pharos.conf>
+
+
+For convenience, there is a bash script source/stop.sh which will stop the dashboard listener and all related scripts.
+
+BEFORE YOU CAN RUN:
+you must first:
+- Integrate FOG into your infrastructure
+- Fill out the needed configuration files
+- Populate the database with your available hosts
+
+
+FOG:
+Our OPNFV infrastructure uses a FOG server to pxe boot, read and write disk images, and otherwise control the hosts we have available for developers.
+FOG is an open source project, and you can view it here: https://fogproject.org/
+FOG provides an easy and scriptable way to completely wipe and write the disks of our hosts.
+ This makes it quick and simple for us to restore our hosts to a known, clean state after a developer has released control of it.
+
+To run the deploy script, you need to:
+ Have a FOG master running
+ Have your hosts registered to the FOG master
+ Have a 'clean' disk image of for each installer / configuration you wish to support.
+ - Fuel, Compass, and JOID all need different distros / versions to run properly
+ - There is a mapping between images and their installers in the installer's config file
+The FOG server must be reachable by whatever machine is running this LaaS software,
+and have network access to PXE boot all of your hosted dev pods.
+
+
+CONFIGURATION:
+INSTALLERS#############################################################################################
+-database Path to the SQLite database for storing host information.
+ Should be the same for all installers in most cases.
+-dhcp_log Path to log file containing DHCP information for dev pods.
+-dhcp_server IP address or hostname of the DHCP server which contains the above log file
+ set to `null` if the same machine will be running dhcp and this project
+-fog
+--api_key The FOG api key. You may instead give the path to a file containing the api key.
+--server The URL of the fog server.
+ ex: http://myServer.com/fog/
+--user_key The FOG api key specific to your user.
+ You may instead give the path to a secrets file containing the key.
+--image_id The id of the image FOG will use when this installer is requested.
+-installer The name of the installer, as seen from the dashboard.
+ `null` will match when no installer is selected, or the `None` installer is..
+-logging_dir The directory to create log files in.
+ Will create the dir if it does not already exist.
+-scenario The default scenario if one is not specified by the user.
+ NOTE: automation of different scenarios are not currently supported.
+ These values are silently ignored.
+-hypervisor_config
+--networks Path to the config file used to define the virtual networks for this installer.
+--vms Path to the config file used to define the virtual machines for this installer.
+-inventory Path to inventory file mapping dashboard host id's to FOG hostnames.
+-vpn_config Path to the vpn config file
+
+
+#########################################################################################################
+
+DOMAINS##################################################################################################
+-jinja-template Path to the jinja xml template used to create libvirt domain xml documents.
+-domains A list of domains. List as many as you want, but be cognizant of hardware limitations
+--disk Path to the qcow2 disk image for this VM
+--interfaces List of interfaces for the vm
+---name The name of the network or bridge that provides this interface
+---type The source of the interface. Either 'bridge' or 'network' is valid, but the bridge
+ must already exist on the host.
+--iso
+---URL Where to fetch the ISO from
+---location Where to save the ISO to
+---used Whether this host will use an iso as a boot drive
+ if `false`, the ISO will not be downloaded
+--memory Memory to allocate to the VM in KiB
+--name libvirt name of VM
+--vcpus How many vcpus to allocate to this host.
+#########################################################################################################
+
+NETWORKS#################################################################################################
+-jinja-template Path to jinja template used to create libvirt XML network documents
+-networks List of networks that will be created
+--brAddr ip address of the bridge on the host
+--brName name of the bridge on the host
+--cidr cidr of the virtual network
+--dhcp dhcp settingg
+---rangeEnd end of DHCP address range
+---rangeStart start of DHCP address range
+---used Whether to enable dhcp for this network. Should probably be false.
+--forward Libvirt network forwarding settings
+---type forwarding type. See libvirt documentation for possible types.
+---used if `false`, the network is isolated.
+--name Name of this network in Libvirt
+--netmask Netmask for this network.
+########################################################################################################
+
+PHAROS##################################################################################################
+-dashboard url of the dashboard. https://labs.opnfv.org is the public OPNFV dashboard
+-database path to database to store booking information.
+ Should be the same db as the host database in most cases
+-default_configs a mappping of installers and their configuration files.
+-inventory path to the inventory file
+-logging_dir Where the pharos dashboard listener should put log files.
+-poling How many times a second the listener will poll the dashboard
+-token Your paros api token. May also be a path to a file containing the token
+#######################################################################################################
+
+VPN####################################################################################################
+NOTE: this all assumes you use LDAP authentication
+-server Domain name of your vpn server
+-authenticaion
+--pass password for your 'admin' user. May also be a path to a secrets file
+--user full dn of your 'admin' user
+-directory
+--root The lowest directory that this program will need to access
+--user The directory where users are stored, relative to the given root dir
+-user
+--objects A list of object classes that vpn users will belong to.
+ Most general class should be on top, and get more specific from there.
+ ex: -top, -inetOrgPerson because `top` is more general
+-database The booking database
+-permanent_users Users that you want to be persistent, even if they have no bookings active
+ ie: your admin users
+ All other users will be deleted when they have no mroe bookings
+#######################################################################################################
+
+INVENTORY##############################################################################################
+This file is used to map the resource id's known by pharos to the hostnames known by FOG.
+for example,
+50: fog-machine-4
+51: fog-machine-5
+52: fog-virtualPod-5.1
+#######################################################################################################
+
+HOW IT WORKS:
+
+0) lab resources are prepared and information is stored in the database
+1) source/listen.py launches a background instance of pharos.py
+ -pharos.py continually polls the dashboard for booking info, and stores it in the database
+2) A known booking begins and pharos.py launches pod_manager.py
+ - pod_manager is launched in a new process, so that the listener continues to poll the dashboard
+ and multiple hosts can be provisioned at once
+3) pod_manager uses FOG to image the host
+4) if requested, pod_manager hands control to deployment_manager to install and deploy OPNFV
+ - deployment_manager instantiates and calls the go() function of the given source/installers/installer subclass
+5) a vpn user is created and random root password is given to the dev pod
+##########The dashboard does not yet support the following actions#############
+6) public ssh key of the user is fetched from the dashboard
+7) user is automatically notified their pod is ready, and given all needed info
+
+
+GENERAL NOTES:
+
+resetDatabase.py relies on FOG to retrieve a list of all hosts available to developers
+
+running:
+ source/resetDatabase.py --both --config <CONFIG_FILE>
+will create a database and populate it.
+WARNING: This will delete existing information if run on a previously initialized database
+
+To aid in visualization and understanding of the resulting topolgy after fully deploying OPNFV and Openstack in
+a development pod, you may review the LaaS_Diagram in this directory.
diff --git a/tools/laas-fog/conf/domain.yaml b/tools/laas-fog/conf/domain.yaml
new file mode 100644
index 00000000..04914e06
--- /dev/null
+++ b/tools/laas-fog/conf/domain.yaml
@@ -0,0 +1,108 @@
+---
+- disk: /vm/master.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: true
+ memory: 8240000
+ name: master
+ vcpus: 4
+
+- disk: /vm/slave1.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: false
+ memory: 8240000
+ name: slave1
+ vcpus: 4
+
+- disk: /vm/slave2.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: false
+ memory: 8240000
+ name: slave2
+ vcpus: 4
+
+- disk: /vm/slave3.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: false
+ memory: 8240000
+ name: slave3
+ vcpus: 4
+
+- disk: /vm/slave4.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: false
+ memory: 8240000
+ name: slave4
+ vcpus: 4
+
+- disk: /vm/slave5.qcow2
+ interfaces:
+ - name: admin
+ type: network
+ - name: public
+ type: network
+ - name: storage
+ type: network
+ - name: management
+ type: network
+ iso:
+ URL: http://artifacts.opnfv.org/fuel/danube/opnfv-danube.2.0.iso
+ location: /vm/fuel.iso
+ used: false
+ memory: 8240000
+ name: slave5
+ vcpus: 4
diff --git a/tools/laas-fog/conf/fuel.yaml b/tools/laas-fog/conf/fuel.yaml
new file mode 100644
index 00000000..0994d862
--- /dev/null
+++ b/tools/laas-fog/conf/fuel.yaml
@@ -0,0 +1,17 @@
+---
+database: /var/OPNFV/hosts.db
+dhcp_log: /var/log/messages
+dhcp_server: null
+fog:
+ api_key: /path/to/fog.key # may also put the key directly here
+ server: http://fogserver.com/fog/
+ user_key: /path/to/fog_user.key
+ image_id: 5
+installer: Fuel
+logging_dir: /var/log/OPNFV/
+scenario: os-nosdn-nofeature-noha
+hypervisor_config:
+ networks: /root/laas/conf/network.yaml
+ vms: /root/laas/conf/domain.yaml
+inventory: /root/laas/conf/inventory.yaml
+vpn_config: /root/laas/conf/vpn.yaml
diff --git a/tools/laas-fog/conf/inventory.yaml b/tools/laas-fog/conf/inventory.yaml
new file mode 100644
index 00000000..9d3d61b0
--- /dev/null
+++ b/tools/laas-fog/conf/inventory.yaml
@@ -0,0 +1,6 @@
+---
+# pharos id : fog name
+# for example:
+1: fog-host-1
+2: fog-host-2
+3: fog-host-3
diff --git a/tools/laas-fog/conf/joid.yaml b/tools/laas-fog/conf/joid.yaml
new file mode 100644
index 00000000..b38dedce
--- /dev/null
+++ b/tools/laas-fog/conf/joid.yaml
@@ -0,0 +1,17 @@
+---
+database: /var/OPNFV/hosts.db
+dhcp_log: /var/log/messages
+dhcp_server: null
+fog:
+ api_key: /path/to/fog.key # may also put the key directly here
+ server: http://fogserver.com/fog/
+ user_key: /path/to/fog_user.key
+ image_id: 12
+installer: Joid
+logging_dir: /var/log/OPNFV/
+scenario: os-nosdn-nofeature-noha
+hypervisor_config:
+ networks: /root/laas/conf/network.yaml
+ vms: /root/laas/conf/domain.yaml
+inventory: /root/laas/conf/inventory.yaml
+vpn_config: /root/laas/conf/vpn.yaml
diff --git a/tools/laas-fog/conf/laas.yaml b/tools/laas-fog/conf/laas.yaml
new file mode 100644
index 00000000..da11a56b
--- /dev/null
+++ b/tools/laas-fog/conf/laas.yaml
@@ -0,0 +1,17 @@
+---
+database: /var/OPNFV/hosts.db
+dhcp_log: /var/log/messages
+dhcp_server: null
+fog:
+ api_key: /path/to/fog.key # may also put the key directly here
+ server: http://fogserver.com/fog/
+ user_key: /path/to/fog_user.key
+ image_id: 5
+installer: null
+logging_dir: /var/log/OPNFV/
+scenario: os-nosdn-nofeature-noha
+hypervisor_config:
+ networks: /root/laas/conf/network.yaml
+ vms: /root/laas/conf/domain.yaml
+inventory: /root/laas/conf/inventory.yaml
+vpn_config: /root/laas/conf/vpn.yaml
diff --git a/tools/laas-fog/conf/network.yaml b/tools/laas-fog/conf/network.yaml
new file mode 100644
index 00000000..61860d56
--- /dev/null
+++ b/tools/laas-fog/conf/network.yaml
@@ -0,0 +1,52 @@
+---
+- brAddr: 10.20.0.1
+ brName: admin-br
+ cidr: 10.20.0.0/24
+ dhcp:
+ rangeEnd: 10.20.0.250
+ rangeStart: 10.20.0.15
+ used: false
+ forward:
+ type: nat
+ used: true
+ name: admin
+ netmask: 255.255.255.0
+
+- brAddr: 10.20.1.1
+ brName: public-br
+ cidr: 10.20.1.0/24
+ dhcp:
+ rangeEnd: 10.20.1.250
+ rangeStart: 10.20.1.15
+ used: false
+ forward:
+ type: nat
+ used: true
+ name: public
+ netmask: 255.255.255.0
+
+- brAddr: 10.20.2.1
+ brName: management-br
+ cidr: 10.20.2.0/24
+ dhcp:
+ rangeEnd: 10.20.2.250
+ rangeStart: 10.20.2.15
+ used: false
+ forward:
+ type: nat
+ used: false
+ name: management
+ netmask: 255.255.255.0
+
+- brAddr: 10.20.3.1
+ brName: storage-br
+ cidr: 10.20.3.0/24
+ dhcp:
+ rangeEnd: 10.20.3.250
+ rangeStart: 10.20.3.15
+ used: false
+ forward:
+ type: nat
+ used: false
+ name: storage
+ netmask: 255.255.255.0
diff --git a/tools/laas-fog/conf/pharos.yaml b/tools/laas-fog/conf/pharos.yaml
new file mode 100644
index 00000000..9fedde12
--- /dev/null
+++ b/tools/laas-fog/conf/pharos.yaml
@@ -0,0 +1,11 @@
+---
+dashboard: https://labs.opnfv.org
+database: /var/OPNFV/laas.db
+default_configs:
+ Fuel: /root/laas/conf/fuel.yaml
+ None: /root/laas/conf/laas.yaml
+ Joid: /rooot/laas/conf/joid.yaml
+inventory: /root/laas/conf/inventory.yaml
+logging_dir: /var/log/OPNFV
+polling: 3
+token: /root/laas/conf/pharos.key
diff --git a/tools/laas-fog/conf/vpn.yaml b/tools/laas-fog/conf/vpn.yaml
new file mode 100644
index 00000000..6f399275
--- /dev/null
+++ b/tools/laas-fog/conf/vpn.yaml
@@ -0,0 +1,15 @@
+---
+server: vpn.domain.com
+authentication:
+ pass: /path/to/keyfile # you may also put the password directly here
+ user: cn=root,o=opnfv,dc=domain,dc=com
+directory:
+ root: o=opnfv,dc=domain,dc=com
+ user: ou=People # relative to the root dir
+user:
+ objects: # listed in ascending order of specificty
+ - top
+ - inetOrgPerson # last object should be a class that only vpn users have
+database: /var/OPNFV/laas.db # same as the pharos api booking db
+permanent_users: # any users you want to be persistent
+ - pberberian
diff --git a/tools/laas-fog/hostScripts/fuelInstall.sh b/tools/laas-fog/hostScripts/fuelInstall.sh
new file mode 100755
index 00000000..c68907d0
--- /dev/null
+++ b/tools/laas-fog/hostScripts/fuelInstall.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+virsh start master
+
+ret=''
+while [ -z "$ret" ]; do
+ echo "Master node is not accepting ssh. Sleeping 15 seconds..."
+ sleep 15
+ ret=$(nmap 10.20.0.2 -PN -p ssh | grep open)
+done
+
+ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ''
+sshpass -p r00tme ssh-copy-id -o stricthostkeychecking=no root@10.20.0.2
+
+ssh root@10.20.0.2 killall fuelmenu
+
+echo "killed fuel menu. Waiting for installation to complete"
+
+ans=''
+while [ -z "$ans" ]; do
+ echo "fuel api unavailable. Sleeping 15 seconds..."
+ sleep 15
+ ans=$(curl http://10.20.0.2:8000 2>/dev/null )
+done
diff --git a/tools/laas-fog/hostScripts/horizonNat.sh b/tools/laas-fog/hostScripts/horizonNat.sh
new file mode 100755
index 00000000..dd6396c6
--- /dev/null
+++ b/tools/laas-fog/hostScripts/horizonNat.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+MYIP=$1
+DESTINATION=$2
+MYBRIDGE=10.20.1.1
+DESTNETWORK=10.20.1.0/24
+PORT=80
+
+iptables -I INPUT 2 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -t nat -I INPUT 1 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -I FORWARD -p tcp --dport "$PORT" -j ACCEPT
+
+iptables -t nat -I PREROUTING -p tcp -d "$MYIP" --dport "$PORT" -j DNAT --to-destination "$DESTINATION:$PORT"
+iptables -t nat -I POSTROUTING -p tcp -s "$DESTINATION" ! -d "$DESTNETWORK" -j SNAT --to-source "$MYIP"
+
+iptables -t nat -I POSTROUTING 2 -d "$DESTINATION" -j SNAT --to-source "$MYBRIDGE"
diff --git a/tools/laas-fog/hostScripts/ipnat.sh b/tools/laas-fog/hostScripts/ipnat.sh
new file mode 100755
index 00000000..b8d97f0d
--- /dev/null
+++ b/tools/laas-fog/hostScripts/ipnat.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+MYIP=$1
+DESTINATION=10.20.0.2
+MYBRIDGE=10.20.0.1
+DESTNETWORK=10.20.0.0/24
+PORTS=(8000 8443)
+
+for PORT in "${PORTS[@]}"; do
+
+ iptables -I INPUT 2 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+ iptables -t nat -I INPUT 1 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+ iptables -I FORWARD -p tcp --dport "$PORT" -j ACCEPT
+
+ iptables -t nat -I PREROUTING -p tcp -d "$MYIP" --dport "$PORT" -j DNAT --to-destination "$DESTINATION:$PORT"
+ iptables -t nat -I POSTROUTING -p tcp -s "$DESTINATION" ! -d "$DESTNETWORK" -j SNAT --to-source "$MYIP"
+
+ iptables -t nat -I POSTROUTING 2 -d "$DESTINATION" -j SNAT --to-source "$MYBRIDGE"
+done
diff --git a/tools/laas-fog/hostScripts/joidInstall.sh b/tools/laas-fog/hostScripts/joidInstall.sh
new file mode 100755
index 00000000..df419c79
--- /dev/null
+++ b/tools/laas-fog/hostScripts/joidInstall.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+# parses the passed scenario
+args=($(echo "$1" | tr "-" "\n"))
+# args is array: [os, nosdn, nofeature, noha]
+
+# the deploy script expects 'none' rather than 'nofeature'
+if [ "nofeature" == "${args[2]}" ]; then
+ args[2]="none"
+fi
+# grabs the joid repo
+git clone "https://gerrit.opnfv.org/gerrit/joid.git"
+# working directory has to be where 03-maasdeploy is
+cd joid/ci
+# virtualy deploy maas
+./03-maasdeploy.sh virtual
+# deploys OPNFV with the given scenario
+./deploy.sh -o newton -s "${args[1]}" -t "${args[3]}" -l default -d xenial -m openstack -f "${args[2]}"
diff --git a/tools/laas-fog/hostScripts/mkDisks.sh b/tools/laas-fog/hostScripts/mkDisks.sh
new file mode 100755
index 00000000..0cbba899
--- /dev/null
+++ b/tools/laas-fog/hostScripts/mkDisks.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+for disk in "$@"; do
+ qemu-img create -f qcow2 "$disk" 100G
+done
diff --git a/tools/laas-fog/hostScripts/vncAllow.sh b/tools/laas-fog/hostScripts/vncAllow.sh
new file mode 100755
index 00000000..98013814
--- /dev/null
+++ b/tools/laas-fog/hostScripts/vncAllow.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+MYIP=X.X.X.X
+PORT="5900:5905"
+iptables -I INPUT 2 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -t nat -I INPUT 1 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -I FORWARD -p tcp --dport "$PORT" -j ACCEPT
+iptables -I OUTPUT -p tcp --dport "$PORT" -j ACCEPT
diff --git a/tools/laas-fog/source/__init__.py b/tools/laas-fog/source/__init__.py
new file mode 100644
index 00000000..7bb515b7
--- /dev/null
+++ b/tools/laas-fog/source/__init__.py
@@ -0,0 +1,17 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
diff --git a/tools/laas-fog/source/api/__init__.py b/tools/laas-fog/source/api/__init__.py
new file mode 100644
index 00000000..7bb515b7
--- /dev/null
+++ b/tools/laas-fog/source/api/__init__.py
@@ -0,0 +1,17 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
diff --git a/tools/laas-fog/source/api/fog.py b/tools/laas-fog/source/api/fog.py
new file mode 100644
index 00000000..62874039
--- /dev/null
+++ b/tools/laas-fog/source/api/fog.py
@@ -0,0 +1,288 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import requests
+import sys
+import time
+
+
+class FOG_Handler:
+ """
+ This class talks with the REST web api for the FOG server.
+
+ TODO: convert prints to logs and remove uneeded pass's
+ """
+
+ def __init__(self, baseURL, fogKey=None, userKey=None):
+ """
+ init function
+ baseURL should be http://fog.ip.or.hostname/fog/
+ fogKey and userKey can optionally be supplied here or later
+ They can be found in fog and provide authentication.
+ """
+ self.baseURL = baseURL
+ self.fogKey = fogKey
+ self.userKey = userKey
+ self.header = {}
+ self.updateHeader()
+
+ def setLogger(self, logger):
+ """
+ saves the refference to the log object as
+ self.log
+ """
+ self.log = logger
+
+ def getUserKeyFromFile(self, path):
+ """
+ reads the user api key from a file
+ """
+ self.userKey = open(path).read()
+ self.updateHeader()
+
+ def getFogKeyFromFile(self, path):
+ """
+ reads the api key from a file
+ """
+ self.fogKey = open(path).read()
+ self.updateHeader()
+
+ def setUserKey(self, key):
+ """
+ sets the user key
+ """
+ self.userKey = key
+ self.updateHeader()
+
+ def setFogKey(self, key):
+ """
+ sets the fog key
+ """
+ self.fogKey = key
+ self.updateHeader()
+
+ def updateHeader(self):
+ """
+ recreates the http header used to talk to the fog api
+ """
+ self.header = {}
+ self.header['fog-api-token'] = self.fogKey
+ self.header['fog-user-token'] = self.userKey
+
+ def setImage(self, host, imgNum):
+ """
+ Sets the image to be used during ghosting to the image
+ with id imgNum. host can either be a hostname or number.
+ """
+ try:
+ host = int(host)
+ except:
+ host = self.getHostNumber(host)
+ url = self.baseURL+"host/"+str(host)
+ host_conf = requests.get(url, headers=self.header).json()
+ host_conf['imageID'] = str(imgNum)
+ requests.put(url+"/edit", headers=self.header, json=host_conf)
+
+ def delTask(self, hostNum):
+ """
+ Tries to delete an existing task for the host
+ with hostNum as a host number
+ """
+ try:
+ url = self.baseURL+'fog/host/'+str(hostNum)+'/cancel'
+ req = requests.delete(url, headers=self.header)
+ if req.status_code == 200:
+ self.log.info("%s", "successfully deleted image task")
+ except Exception:
+ self.log.exception("Failed to delete the imaging task!")
+
+ def getHostMac(self, hostname):
+ """
+ returns the primary mac address if the given host.
+ """
+ try:
+ hostNum = int(self.getHostNumber(hostname))
+ url = self.baseURL + "host/"+str(hostNum)
+ req = requests.get(url, headers=self.header)
+ macAddr = req.json()['primac']
+ return macAddr
+ except Exception:
+ self.log.exception('%s', "Failed to connect to the FOG server")
+
+ def getHostNumber(self, hostname):
+ """
+ returns the host number of given host
+ """
+ try:
+ req = requests.get(self.baseURL+"host", headers=self.header)
+ hostData = req.json()
+ if hostData is not None:
+ for hostDict in hostData['hosts']:
+ if hostname == hostDict['name']:
+ return hostDict['id']
+ return -1
+ except Exception:
+ self.log.exception('%s', "Failed to connect to the FOG server")
+
+ def imageHost(self, hostName, recurse=False):
+ """
+ Schedules an imaging task for the given host.
+ This automatically uses the "associated" disk image.
+ To support extra installers, I will need to create
+ a way to change what that image is before calling
+ this method.
+ """
+ num = str(self.getHostNumber(hostName))
+ url = self.baseURL+'host/'+num+'/task'
+
+ try:
+ req = requests.post(
+ url,
+ headers=self.header,
+ json={"taskTypeID": 1}
+ )
+ if req.status_code == 200:
+ self.log.info("%s", "Scheduled image task for host")
+ except Exception:
+ if recurse: # prevents infinite loop
+ self.log.exception("%s", "Failed to schedule task. Exiting")
+ sys.exit(1)
+ self.log.warning("%s", "Failed to schedule host imaging")
+ self.log.warning("%s", "Trying to delete existing image task")
+ self.delTask(num)
+ self.imageHost(num, recurse=True)
+
+ def waitForHost(self, host):
+ """
+ tracks the imaging task to completion.
+ """
+ while True:
+ imageTask = self.getImagingTask(host)
+ if imageTask is None:
+ self.log.info("%s", "Imaging complete")
+ return
+ state = int(imageTask['stateID'])
+ if state == 1:
+ self.log.info("%s", "Waiting for host to check in")
+ self.waitForTaskToActive(host)
+ continue
+ if state == 3:
+ self.waitForTaskToStart(host)
+ self.waitForImaging(host)
+ continue
+ time.sleep(8)
+
+ def waitForImaging(self, host):
+ """
+ Once the host begins being imaged, this tracks progress.
+ """
+ # print "Host has begun the imaging process\n"
+ while True:
+ task = self.getImagingTask(host)
+ if task is None:
+ return
+ per = str(task['percent'])
+ self.log.info("%s percent done imaging", per)
+ time.sleep(15)
+
+ def waitForTaskToActive(self, host):
+ """
+ Waits for the host to reboot and pxe boot
+ into FOG
+ """
+ while True:
+ try:
+ task = self.getImagingTask(host)
+ except:
+ pass
+ state = int(task['stateID'])
+ if state == 1:
+ time.sleep(4)
+ else:
+ return
+
+ def waitForTaskToStart(self, host):
+ """
+ waits for the task to start and imaging to begin.
+ """
+ while True:
+ try:
+ per = str(self.getImagingTask(host)['percent'])
+ except:
+ pass
+ if per.strip() == '':
+ time.sleep(1)
+ else:
+ return
+
+ def getImagingTask(self, host):
+ """
+ Sorts through all current tasks to find the image task
+ associated with the given host.
+ """
+ try:
+ taskList = requests.get(
+ self.baseURL+'task/current',
+ headers=self.header)
+ taskList = taskList.json()['tasks']
+ imageTask = None
+ for task in taskList:
+ hostname = str(task['host']['name'])
+ if hostname == host and int(task['typeID']) == 1:
+ imageTask = task
+ return imageTask
+ except Exception:
+ self.log.exception("%s", "Failed to talk to FOG server")
+ sys.exit(1)
+
+ def getHosts(self):
+ """
+ returns a list of all hosts
+ """
+ req = requests.get(self.baseURL+"host", headers=self.header)
+ return req.json()['hosts']
+
+ def getHostsinGroup(self, groupName):
+ """
+ returns a list of all hosts in groupName
+ """
+ groupID = None
+ groups = requests.get(self.baseURL+"group", headers=self.header)
+ groups = groups.json()['groups']
+ for group in groups:
+ if groupName.lower() in group['name'].lower():
+ groupID = group['id']
+ if groupID is None:
+ return
+ hostIDs = []
+ associations = requests.get(
+ self.baseURL+"groupassociation",
+ headers=self.header
+ )
+ associations = associations.json()['groupassociations']
+ for association in associations:
+ if association['groupID'] == groupID:
+ hostIDs.append(association['hostID'])
+
+ hosts = []
+ for hostID in hostIDs:
+ hosts.append(requests.get(
+ self.baseURL+"host/"+str(hostID),
+ headers=self.header
+ ).json())
+ return hosts
diff --git a/tools/laas-fog/source/api/fuel_api.py b/tools/laas-fog/source/api/fuel_api.py
new file mode 100644
index 00000000..01278000
--- /dev/null
+++ b/tools/laas-fog/source/api/fuel_api.py
@@ -0,0 +1,306 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import requests
+import time
+import sys
+
+
+class Fuel_api:
+
+ def __init__(self, url, logger, user="admin", password="admin"):
+ """
+ url is the url of the fog api in the form
+ http://ip.or.host.name:8000/
+ logger is a reference to the logger
+ the default creds for fuel is admin/admin
+ """
+ self.logger = logger
+ self.base = url
+ self.user = user
+ self.password = password
+ self.header = {"Content-Type": "application/json"}
+
+ def getKey(self):
+ """
+ authenticates with the user and password
+ to get a keystone key, used in the headers
+ from here on to talk to fuel.
+ """
+ url = self.base + 'keystone/v2.0/tokens/'
+ reqData = {"auth": {
+ "tenantName": self.user,
+ "passwordCredentials": {
+ "username": self.user,
+ "password": self.password
+ }
+ }}
+ self.logger.info("Retreiving keystone token from %s", url)
+ token = requests.post(url, headers=self.header, json=reqData)
+ self.logger.info("Received response code %d", token.status_code)
+ self.token = token.json()['access']['token']['id']
+ self.header['X-Auth-Token'] = self.token
+
+ def getNotifications(self):
+ """
+ returns the fuel notifications
+ """
+ url = self.base+'/api/notifications'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def waitForBootstrap(self):
+ """
+ Waits for the bootstrap image to build.
+ """
+ while True:
+ time.sleep(30)
+ notes = self.getNotifications()
+ for note in notes:
+ if "bootstrap image building done" in note['message']:
+ return
+
+ def getNodes(self):
+ """
+ returns a list of all nodes booted into fuel
+ """
+ url = self.base+'api/nodes'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getID(self, mac):
+ """
+ gets the fuel id of node with given mac
+ """
+ for node in self.getNodes():
+ if node['mac'] == mac:
+ return node['id']
+
+ def getNetID(self, name, osid):
+ """
+ gets the id of the network with name
+ """
+ url = self.base+'api/clusters/'
+ url += str(osid)+'/network_configuration/neutron'
+ try:
+ req = requests.get(url, headers=self.header)
+ nets = req.json()['networks']
+ for net in nets:
+ if net['name'] == name:
+ return net['id']
+ return -1
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def createOpenstack(self):
+ """
+ defines a new openstack environment in fuel.
+ """
+ url = self.base+'api/clusters'
+ data = {
+ "nodes": [],
+ "tasks": [],
+ "name": "OpenStack",
+ "release_id": 2,
+ "net_segment_type": "vlan"
+ }
+ try:
+ req = requests.post(url, json=data, headers=self.header)
+ return req.json()['id']
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def simpleNetDict(self, osID):
+ """
+ returns a simple dict of network names and id numbers
+ """
+ nets = self.getNetworks(osID)
+ netDict = {}
+ targetNets = ['admin', 'public', 'storage', 'management']
+ for net in nets['networks']:
+ for tarNet in targetNets:
+ if tarNet in net['name']:
+ netDict[tarNet] = net['id']
+ return netDict
+
+ def getNetworks(self, osID):
+ """
+ Returns the pythonizezd json of the openstack networks
+ """
+ url = self.base + 'api/clusters/'
+ url += str(osID)+'/network_configuration/neutron/'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def uploadNetworks(self, netJson, osID):
+ """
+ configures the networks of the openstack
+ environment with id osID based on netJson
+ """
+ url = self.base+'api/clusters/'
+ url += str(osID)+'/network_configuration/neutron'
+ try:
+ req = requests.put(url, headers=self.header, json=netJson)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def addNodes(self, clusterID, nodes):
+ """
+ Adds the nodes into this openstack environment.
+ nodes is valid json
+ """
+ url = self.base + 'api/clusters/'+str(clusterID)+'/assignment'
+ try:
+ req = requests.post(url, headers=self.header, json=nodes)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getIfaces(self, nodeID):
+ """
+ returns the pythonized json describing the
+ interfaces of given node
+ """
+ url = self.base + 'api/nodes/'+str(nodeID)+'/interfaces'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def setIfaces(self, nodeID, ifaceJson):
+ """
+ configures the interfaces of node with id nodeID
+ with ifaceJson
+ ifaceJson is valid json that fits fuel's schema for ifaces
+ """
+ url = self.base+'/api/nodes/'+str(nodeID)+'/interfaces'
+ try:
+ req = requests.put(url, headers=self.header, json=ifaceJson)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getTasks(self):
+ """
+ returns a list of all tasks
+ """
+ url = self.base+"/api/tasks/"
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def waitForTask(self, uuid):
+ """
+ Tracks the progress of task with uuid and
+ returns once the task finishes
+ """
+ progress = 0
+ while progress < 100:
+ for task in self.getTasks():
+ if task['uuid'] == uuid:
+ progress = task['progress']
+ self.logger.info("Task is %s percent done", str(progress))
+ time.sleep(20)
+ # Task may hang a minute at 100% without finishing
+ while True:
+ for task in self.getTasks():
+ if task['uuid'] == uuid and not task['status'] == "ready":
+ time.sleep(10)
+ elif task['uuid'] == uuid and task['status'] == "ready":
+ return
+
+ def getHorizonIP(self, osid):
+ """
+ returns the ip address of the horizon dashboard.
+ Horizon always takes the first ip after the public router's
+ """
+ url = self.base+'api/clusters/'
+ url += str(osid)+'/network_configuration/neutron/'
+ try:
+ req = requests.get(url, headers=self.header)
+ routerIP = req.json()['vips']['vrouter_pub']['ipaddr'].split('.')
+ routerIP[-1] = str(int(routerIP[-1])+1)
+ return '.'.join(routerIP)
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def deployOpenstack(self, clusterID):
+ """
+ Once openstack and the nodes are configured,
+ this method actually deploys openstack.
+ It takes a while.
+ """
+ # First, we need to provision the cluster
+ url = self.base+'/api/clusters/'+str(clusterID)+'/provision'
+ req = requests.put(url, headers=self.header)
+ if req.status_code < 300:
+ self.logger.info('%s', "Sent provisioning task")
+ else:
+ err = "failed to provision Openstack Environment"
+ self.logger.error('%s', err)
+ sys.exit(1)
+
+ taskUID = ''
+ tasks = self.getTasks()
+ for task in tasks:
+ if task['name'] == "provision" and task['cluster'] == clusterID:
+ taskUID = task['uuid']
+
+ self.waitForTask(taskUID)
+
+ # Then, we deploy cluster
+ url = self.base + '/api/clusters/'+str(clusterID)+'/deploy'
+ req = requests.put(url, headers=self.header)
+ if req.status_code < 300:
+ self.logger.info('%s', "Sent deployment task")
+ taskUID = ''
+ tasks = self.getTasks()
+ for task in tasks:
+ if 'deploy' in task['name'] and task['cluster'] == clusterID:
+ taskUID = task['uuid']
+ if len(taskUID) > 0:
+ self.waitForTask(taskUID)
diff --git a/tools/laas-fog/source/api/libvirt_api.py b/tools/laas-fog/source/api/libvirt_api.py
new file mode 100644
index 00000000..4e19736f
--- /dev/null
+++ b/tools/laas-fog/source/api/libvirt_api.py
@@ -0,0 +1,331 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import libvirt
+import time
+import xml.dom
+import xml.dom.minidom
+from domain import Domain
+from network import Network
+from utilities import Utilities
+
+
+class Libvirt:
+ """
+ This class talks to the Libvirt api.
+ Given a config file, this class should create all networks and
+ domains.
+
+ TODO: convert prints to logging and remove uneeded pass statements
+ """
+
+ def __init__(self, hostAddr, net_conf=None, dom_conf=None):
+ """
+ init function
+ hostAddr is the ip address of the host
+ net_conf and dom_conf are the paths
+ to the config files
+ """
+ self.host = hostAddr
+ self.URI = "qemu+ssh://root@"+str(hostAddr)+"/system"
+ self.hypervisor = None
+ self.domains = []
+ self.networks = []
+ self.net_conf = net_conf
+ self.dom_conf = dom_conf
+
+ def setLogger(self, log):
+ """
+ Saves the logger in self.log
+ """
+ self.log = log
+
+ def bootMaster(self):
+ """
+ starts the previously defined master node
+ """
+ for dom in self.domains:
+ if 'master' in dom.name():
+ try:
+ dom.create()
+ except Exception:
+ pass
+
+ def bootSlaves(self):
+ """
+ boots every defined vm with 'slave' in its name
+ """
+ for dom in self.domains:
+ if 'slave' in dom.name():
+ try:
+ dom.create()
+ self.log.info("Booting %s", dom.name())
+ except Exception:
+ self.log.exception("%s", "failed to boot domain")
+ time.sleep(5)
+
+ def getMacs(self, domName):
+ """
+ returns a dictionary with a network name
+ mapped to the mac address of the domain on that net
+ """
+ try:
+ dom = self.hypervisor.lookupByName(domName)
+ xmlDesc = dom.XMLDesc(0)
+ parsedXML = xml.dom.minidom.parseString(xmlDesc)
+ interfacesXML = parsedXML.getElementsByTagName('interface')
+ netDict = {}
+ for iface in interfacesXML:
+ src = iface.getElementsByTagName('source')[0]
+ mac = iface.getElementsByTagName('mac')[0]
+ netDict[src] = mac
+ return netDict
+ except Exception:
+ self.log.exception("%s", "Domain not found")
+
+ def defineVM(self, xmlConfig):
+ """
+ Generic method to define a persistent vm with the
+ given config.
+ Assumes that self.hypervisor is already connected.
+ """
+ if self.checkForVM(xmlConfig):
+ vm = self.hypervisor.defineXML(xmlConfig)
+ if vm is None:
+ name = self.getName(xmlConfig)
+ self.log.error("Failed to define vm %s. exiting", name)
+ exit(1)
+ else:
+ self.log.info("Successfully created vm %s", vm.name())
+ pass
+ self.domains.append(vm)
+
+ def checkForVM(self, xmlConfig):
+ """
+ Checks if another vm with the same name exists
+ on the remote host already. If it does, it will
+ delete that vm
+ """
+ allGood = False
+ vms = self.hypervisor.listAllDomains(0)
+ names = []
+ for dom in vms:
+ names.append(dom.name())
+ vmName = Utilities.getName(xmlConfig)
+ if vmName in names:
+ self.log.warning("domain %s already exists", vmName)
+ self.log.warning("%s", "Atempting to delete it")
+ self.deleteVM(vmName)
+ allGood = True
+ else:
+ allGood = True
+ return allGood
+
+ def deleteVM(self, name):
+ """
+ removes the given vm from the remote host
+ """
+ try:
+ vm = self.hypervisor.lookupByName(name)
+ except:
+ return
+ active = vm.isActive()
+ persistent = vm.isPersistent()
+ if active:
+ try:
+ vm.destroy()
+ except:
+ self.log.exception("%s", "Failed to destroy vm")
+
+ if persistent:
+ try:
+ vm.undefine()
+ except:
+ self.log.exception("%s", "Failed to undefine domain")
+ pass
+
+ def openConnection(self):
+ """
+ opens a connection to the remote host
+ and stores it in self.hypervisor
+ """
+ self.log.info("Attempting to connect to libvirt at %s", self.host)
+ try:
+ hostHypervisor = libvirt.open(self.URI)
+ except:
+ self.log.warning(
+ "Failed to connect to %s. Trying again", self.host
+ )
+ time.sleep(5)
+ try:
+ hostHypervisor = libvirt.open(self.URI)
+ except:
+ self.log.exception("Cannot connect to %s. Exiting", self.host)
+ exit(1)
+
+ if hostHypervisor is None:
+ self.log.error("Failed to connect to %s. Exiting", self.host)
+ exit(1)
+ self.hypervisor = hostHypervisor
+
+ def restartVM(self, vm):
+ """
+ causes the given vm to reboot
+ """
+ dom = self.hypervisor.lookupByName(vm)
+ dom.destroy()
+ time.sleep(15)
+ dom.create()
+
+ def close(self):
+ """
+ Closes connection to remote hypervisor
+ """
+ self.log.info("Closing connection to the hypervisor %s", self.host)
+ self.hypervisor.close()
+
+ def defineAllDomains(self, path):
+ """
+ Defines a domain from all the xml files in a directory
+ """
+ files = Utilities.getXMLFiles(path)
+ definitions = []
+ for xml_desc in files:
+ definitions.append(xml_desc.read())
+
+ for definition in definitions:
+ self.defineVM(definition)
+
+ def createAllNetworks(self, path):
+ """
+ Creates a network from all xml files in a directory
+ """
+ files = Utilities.getXMLFiles(path)
+ definitions = []
+ for xml_desc in files:
+ definitions.append(Utilities.fileToString(xml_desc))
+
+ for definition in definitions:
+ self.createNet(definition)
+
+ def createNet(self, config):
+ """
+ creates the network on the remote host
+ config is the xml in string representation
+ that defines the network
+ """
+ if self.checkNet(config):
+ network = self.hypervisor.networkDefineXML(config)
+
+ if network is None:
+ name = self.getName(config)
+ self.log.warning("Failed to define network %s", name)
+ network.create()
+ if network.isActive() == 1:
+ net = network.name()
+ self.log.info("Successfully defined network %s", net)
+ self.networks.append(network)
+
+ def checkNet(self, config):
+ """
+ Checks if another net with the same name exists, and
+ deletes that network if one is found
+ """
+ allGood = False
+ netName = Utilities.getName(config)
+ if netName not in self.hypervisor.listNetworks():
+ return True
+ else: # net name is already used
+ self.log.warning(
+ "Network %s already exists. Trying to delete it", netName
+ )
+ network = self.hypervisor.networkLookupByName(netName)
+ self.deleteNet(network)
+ allGood = True
+ return allGood
+
+ def deleteNet(self, net):
+ """
+ removes the given network from the host
+ """
+ active = net.isActive()
+ persistent = net.isPersistent()
+ if active:
+ try:
+ net.destroy()
+ except:
+ self.log.warning("%s", "Failed to destroy network")
+
+ if persistent:
+ try:
+ net.undefine()
+ except:
+ self.log.warning("%s", "Failed to undefine network")
+
+ def go(self):
+ """
+ This method does all the work of this class,
+ Parsing the net and vm config files and creating
+ all the requested nets/domains
+ returns a list of all networks and a list of all domains
+ as Network and Domain objects
+ """
+ nets = self.makeNetworks(self.net_conf)
+ doms = self.makeDomains(self.dom_conf)
+ return doms, nets
+
+ def makeNetworks(self, conf):
+ """
+ Given a path to a config file, this method
+ parses the config and creates all requested networks,
+ and returns them in a list of Network objects
+ """
+ networks = []
+ definitions = Network.parseConfigFile(conf)
+ for definition in definitions:
+ network = Network(definition)
+ networks.append(network)
+ self.createNet(network.toXML())
+ return networks
+
+ def makeDomains(self, conf):
+ """
+ Given a path to a config file, this method
+ parses the config and creates all requested vm's,
+ and returns them in a list of Domain objects
+ """
+ domains = []
+ definitions = Domain.parseConfigFile(conf)
+ for definition in definitions:
+ domain = Domain(definition)
+ domains.append(domain)
+ self.defineVM(domain.toXML())
+ return domains
+
+ @staticmethod
+ def getName(xmlString):
+ """
+ given xml with a name tag, this returns the value of name
+ eg:
+ <name>Parker</name>
+ returns 'Parker'
+ """
+ xmlDoc = xml.dom.minidom.parseString(xmlString)
+ nameNode = xmlDoc.documentElement.getElementsByTagName('name')
+ name = str(nameNode[0].firstChild.nodeValue)
+ return name
diff --git a/tools/laas-fog/source/api/vpn.py b/tools/laas-fog/source/api/vpn.py
new file mode 100644
index 00000000..336a681d
--- /dev/null
+++ b/tools/laas-fog/source/api/vpn.py
@@ -0,0 +1,235 @@
+from abc import ABCMeta, abstractmethod
+import ldap
+import os
+import random
+from base64 import b64encode
+from database import BookingDataBase
+
+
+class VPN_BaseClass:
+ """
+ the vpn handler abstract class / interface
+
+ """
+ __metaclass__ = ABCMeta
+
+ @abstractmethod
+ def __init__(self, config):
+ """
+ config is the parsed vpn.yaml file
+ """
+ pass
+
+ @abstractmethod
+ def makeNewUser(self, user=None):
+ """
+ This method is called when a vpn user is needed.
+ This method should create a vpn user in whatever
+ runs the vpn in our infrastructure. returns the
+ credentials for the vpn user and some uid
+ that will be associated with the booking in the
+ database. This uid is used to track the vpn user and
+ to delete the user when there are no bookings associated
+ with that uid.
+ """
+ user = "username"
+ passwd = "password"
+ uid = "some way for you to identify this user in the database"
+ return user, passwd, uid
+
+ @abstractmethod
+ def removeOldUsers(self):
+ """
+ checks the list of all vpn users against a list of
+ vpn users associated with active bookings and removes
+ users who dont have an active booking
+
+ If you want your vpn accounts to be persistent,
+ you can just ignore this
+ """
+ pass
+
+
+names = [
+ 'frodo baggins', 'samwise gamgee', 'peregrin took', 'meriadoc brandybuck',
+ 'bilbo baggins', 'gandalf grey', 'aragorn dunadan', 'arwen evenstar',
+ 'saruman white', 'pippin took', 'merry brandybuck', 'legolas greenleaf',
+ 'gimli gloin', 'anakin skywalker', 'padme amidala', 'han solo',
+ 'jabba hut', 'mace windu', 'sount dooku', 'qui-gon jinn',
+ 'admiral ackbar', 'emperor palpatine'
+]
+
+
+class VPN:
+ """
+ This class communicates with the ldap server to manage vpn users.
+ This class extends the above ABC, and implements the makeNewUser,
+ removeOldUser, and __init__ abstract functions you must override to
+ extend the VPN_BaseClass
+ """
+
+ def __init__(self, config):
+ """
+ init takes the parsed vpn config file as an arguement.
+ automatically connects and authenticates on the ldap server
+ based on the configuration file
+ """
+ self.config = config
+ server = config['server']
+ self.uri = "ldap://"+server
+
+ self.conn = None
+ user = config['authentication']['user']
+ pswd = config['authentication']['pass']
+ if os.path.isfile(pswd):
+ pswd = open(pswd).read()
+ self.connect(user, pswd)
+
+ def connect(self, root_dn, root_pass):
+ """
+ Opens a connection to the server in the config file
+ and authenticates as the given user
+ """
+ self.conn = ldap.initialize(self.uri)
+ self.conn.simple_bind_s(root_dn, root_pass)
+
+ def addUser(self, full_name, passwd):
+ """
+ Adds a user to the ldap server. Creates the new user with the classes
+ and in the directory given in the config file.
+ full_name should be two tokens seperated by a space. The first token
+ will become the username
+ private helper function for the makeNewUser()
+ """
+ first = full_name.split(' ')[0]
+ last = full_name.split(' ')[1]
+ user_dir = self.config['directory']['user']
+ user_dir += ','+self.config['directory']['root']
+ dn = "uid=" + first + ',' + user_dir
+ record = [
+ ('objectclass', ['top', 'inetOrgPerson']),
+ ('uid', first),
+ ('cn', full_name),
+ ('sn', last),
+ ('userpassword', passwd),
+ ('ou', self.config['directory']['user'].split('=')[1])
+ ]
+ self.conn.add_s(dn, record)
+ return dn
+
+ def makeNewUser(self, name=None):
+ """
+ creates a new user in the ldap database, with the given name
+ if supplied. If no name is given, we will try to select from the
+ pre-written list above, and will resort to generating a random string
+ as a username if the preconfigured names are all taken.
+ Returns the username and password the user needs to authenticate, and
+ the dn that we can use to manage the user.
+ """
+ if name is None:
+ i = 0
+ while not self.checkName(name):
+ i += 1
+ if i == 20:
+ name = self.randoString(8)
+ name += ' '+self.randoString(8)
+ break # generates a random name to prevent infinite loop
+ name = self.genUserName()
+ passwd = self.randoString(15)
+ dn = self.addUser(name, passwd)
+ return name, passwd, dn
+
+ def checkName(self, name):
+ """
+ returns true if the name is available
+ """
+ if name is None:
+ return False
+ uid = name.split(' ')[0]
+ base = self.config['directory']['user'] + ','
+ base += self.config['directory']['root']
+ filtr = '(uid=' + uid + ')'
+ timeout = 5
+ ans = self.conn.search_st(
+ base,
+ ldap.SCOPE_SUBTREE,
+ filtr,
+ timeout=timeout
+ )
+ return len(ans) < 1
+
+ @staticmethod
+ def randoString(n):
+ """
+ uses /dev/urandom to generate a random string of length n
+ """
+ n = int(n)
+ # defines valid characters
+ alpha = 'abcdefghijklmnopqrstuvwxyz'
+ alpha_num = alpha
+ alpha_num += alpha.upper()
+ alpha_num += "0123456789"
+
+ # generates random string from /dev/urandom
+ rnd = b64encode(os.urandom(3*n)).decode('utf-8')
+ random_string = ''
+ for char in rnd:
+ if char in alpha_num:
+ random_string += char
+ return str(random_string[:n])
+
+ def genUserName(self):
+ """
+ grabs a random name from the list above
+ """
+ i = random.randint(0, len(names) - 1)
+ return names[i]
+
+ def deleteUser(self, dn):
+ self.conn.delete(dn)
+
+ def getAllUsers(self):
+ """
+ returns all the user dn's in the ldap database in a list
+ """
+ base = self.config['directory']['user'] + ','
+ base += self.config['directory']['root']
+ filtr = '(objectclass='+self.config['user']['objects'][-1]+')'
+ timeout = 10
+ ans = self.conn.search_st(
+ base,
+ ldap.SCOPE_SUBTREE,
+ filtr,
+ timeout=timeout
+ )
+ users = []
+ for user in ans:
+ users.append(user[0]) # adds the dn of each user
+ return users
+
+ def removeOldUsers(self):
+ """
+ removes users from the ldap server who dont have any active bookings.
+ will not delete a user if their uid's are named in the config
+ file as permanent users.
+ """
+ db = self.config['database']
+ # the dn of all users who have an active booking
+ active_users = BookingDataBase(db).getVPN()
+ all_users = self.getAllUsers()
+ for user in all_users:
+ # checks if they are a permanent user
+ if self.is_permanent_user(user):
+ continue
+ # deletes the user if they dont have an active booking
+ if user not in active_users:
+ self.deleteUser(user)
+
+ def is_permanent_user(self, dn):
+ for user in self.config['permanent_users']:
+ if (user in dn) or (dn in user):
+ return True
+ return False
+
+
+VPN_BaseClass.register(VPN)
diff --git a/tools/laas-fog/source/database.py b/tools/laas-fog/source/database.py
new file mode 100644
index 00000000..ca7e5c89
--- /dev/null
+++ b/tools/laas-fog/source/database.py
@@ -0,0 +1,296 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+import sqlite3
+import sys
+import time
+
+
+class HostDataBase:
+ """
+ This class talks with a simple sqlite database and can select a free host
+ when one is needed.
+ The layout of the database is:
+ TABLE host:
+ name <hostname> status <status_code> book_start
+ <Unix timestamp> book_end <Unix timestamp>
+ status_codes:
+ 0 - idle
+ 1 - deploying
+ 2 - deployed, in use
+ 3 - expired, ready to be reset
+ """
+
+ def __init__(self, path):
+ """
+ init function. Will create the file at the end of path
+ if it doesnt already exist
+ """
+ self.database = sqlite3.connect(path)
+ self.cursor = self.database.cursor()
+
+ def resetHosts(self, hosts):
+ """
+ Recreates the host table in the database.
+ WILL ERASE ALL DATA. USE WITH CAUTION.
+ """
+ try:
+ self.cursor.execute("DROP TABLE hosts")
+ self.createTable()
+ except:
+ pass
+
+ for host in hosts:
+ self.addHost(host)
+
+ def createTable(self):
+ """
+ This method creates the table hosts with
+ a name and status field
+ """
+ self.cursor.execute("CREATE TABLE hosts (name text, status integer)")
+ self.database.commit()
+
+ def addHost(self, name):
+ """
+ Adds a host with name to the available hosts.
+ When first added, the host is assumed to be idle.
+ """
+ host = (name, )
+ self.cursor.execute("INSERT INTO hosts VALUES (?, 0) ", host)
+ self.database.commit()
+
+ def getHost(self, requested=None):
+ """
+ Returns the name of an available host.
+ If a host is specifically requested,
+ that host is returned.
+ If the requested host is not available,
+ this method will throw an error.
+ If no host is specificaly requested,
+ the next available host is returned.
+ """
+ self.cursor.execute("SELECT name FROM hosts WHERE status = 0")
+ hostList = self.cursor.fetchall()
+ if len(hostList) < 1:
+ # throw and exception
+ sys.exit(1)
+ host = None
+ if requested is not None:
+ if (requested, ) in hostList and self.hostIsIdle(requested):
+ host = requested # If requested, exists, and idle, return it
+ else:
+ sys.exit(1)
+ else:
+ host = hostList[0][0]
+ self.makeHostBusy(host)
+ return host
+
+ def makeHostBusy(self, name):
+ """
+ makes the status of host 'name' equal 1,
+ making it 'busy'
+ """
+ host = (name, )
+ self.cursor.execute("UPDATE hosts SET status = 1 WHERE name=?", host)
+ self.database.commit()
+
+ def makeHostDeployed(self, name):
+ """
+ makes the status of host 'name' equal 2,
+ making it 'deployed' and/or in use
+ """
+ host = (name, )
+ self.cursor.execute("UPDATE hosts SET status = 2 WHERE name=?", host)
+ self.database.commit()
+
+ def makeHostExpired(self, name):
+ """
+ makes the status of host 'name' equal 3,
+ meaning its booking has ended and needs to be cleaned.
+ """
+ host = (name, )
+ self.cursor.execute("UPDATE hosts SET status = 3 WHERE name=?", host)
+ self.database.commit()
+
+ def getExpiredHosts(self):
+ """
+ returns a list of all hosts with an expired booking that
+ need to be cleaned.
+ """
+ self.cursor.execute("SELECT name FROM hosts where status = 3")
+ host_tuples = self.cursor.fetchall()
+ hosts = []
+ for host in host_tuples:
+ hosts.append(host[0])
+ return hosts # returns list of strings, not tuples
+
+ def hostIsBusy(self, name):
+ """
+ returns True if the host is not idle
+ """
+ host = (name, )
+ self.cursor.execute("SELECT status FROM hosts WHERE name=?", host)
+ stat = self.cursor.fetchone()[0]
+ if stat < 1:
+ return False
+ return True
+
+ def hostIsIdle(self, name):
+ """
+ returns True if the host is idle.
+ """
+ return not self.hostIsBusy(name)
+
+ def getAllHosts(self):
+ """
+ returns the whole host database.
+ """
+ self.cursor.execute("SELECT * FROM hosts")
+ return self.cursor.fetchall()
+
+ def close(self):
+ """
+ commits and closes connection to the database file.
+ """
+ self.database.commit()
+ self.database.close()
+
+
+class BookingDataBase:
+ """
+ Database to hold all active bookings for our servers.
+ Database contains table bookings - can be same or different
+ db file as the host database
+ bookings contains a field for every json key from the pharos dashboard,
+ plus a "status" integer which is either
+ 0 - waiting to start
+ 1 - started
+ 2 - booking over
+
+ As written, the pharos listener will immediately store all bookings that
+ are both for your dev pods and not
+ yet over, regardless of when the booking starts. Once the booking ends
+ and the dev pod is cleaned, the booking is deleted to save space and cpu.
+ """
+
+ def __init__(self, path):
+ """
+ creates a BookingDataBase object with the database located
+ at path. if path does not yet exist, it will be created.
+ """
+ self.database = sqlite3.connect(path)
+ self.cursor = self.database.cursor()
+
+ def createTable(self):
+ """
+ Creates table in the database to store booking information
+ """
+ try:
+ self.cursor.execute("DROP TABLE bookings")
+ except:
+ pass
+ self.cursor.execute("""CREATE TABLE bookings
+ (id integer, resource_id integer, start double, end double,
+ installer_name text, scenario_name text,
+ purpose text, status integer, vpn text)""")
+ self.database.commit()
+
+ def checkAddBooking(self, booking):
+ """
+ This method accepts a JSON booking definition from the dashboard
+ api and adds it to the database if it does not already exist.
+ """
+ # first, check if booking is already expired
+ if time.time() > booking['end']:
+ return
+ # check if booking is in database already
+ b_id = (booking['id'], )
+ self.cursor.execute("SELECT * FROM bookings WHERE id=?", b_id)
+ if len(self.cursor.fetchall()) > 0: # booking already in the db
+ return
+ tup = (
+ booking['id'],
+ booking['resource_id'],
+ booking['start'],
+ booking['end'],
+ booking['installer_name'],
+ booking['scenario_name'],
+ booking['purpose'],
+ 0,
+ ''
+ )
+ self.cursor.execute(
+ "INSERT INTO bookings VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", tup)
+ self.database.commit()
+
+ def removeBooking(self, idNum):
+ """
+ deletes booking with given id from the database.
+ """
+ booking_id = (idNum, )
+ self.cursor.execute("DELETE FROM bookings WHERE id=?", booking_id)
+
+ def getBookings(self):
+ """
+ returns a list of all bookings.
+ """
+ self.cursor.execute("SELECT * FROM bookings")
+ return self.cursor.fetchall()
+
+ def setStatus(self, booking_id, status):
+ """
+ sets the status of the booking with booking id booking_id.
+ as noted above, the status codes are:
+ 0 - not yet started
+ 1 - started, but not yet over
+ 2 - over, expired
+ """
+ data = (status, booking_id)
+ self.cursor.execute("UPDATE bookings SET status=? WHERE id=?", data)
+ self.database.commit()
+
+ def setVPN(self, resource, uid):
+ data = (uid, resource, 1)
+ self.cursor.execute(
+ "UPDATE bookings SET vpn=? WHERE resource_id=? AND status=?",
+ data
+ )
+ self.database.commit()
+
+ def getVPN(self):
+ """
+ returns a list of all vpn users associated with current
+ bookings.
+ """
+ self.cursor.execute("SELECT vpn FROM bookings WHERE status=1")
+ users_messy = self.cursor.fetchall()
+ users = []
+ for user in users_messy:
+ user = user[0] # get string rather than tuple
+ user = user.strip()
+ if len(user) < 1:
+ continue
+ users.append(user) # a list of non-empty strings
+ return users
+
+ def close(self):
+ """
+ commits changes and closes connection to db file.
+ """
+ self.database.commit()
+ self.database.close()
diff --git a/tools/laas-fog/source/deploy.py b/tools/laas-fog/source/deploy.py
new file mode 100755
index 00000000..a9c5e04f
--- /dev/null
+++ b/tools/laas-fog/source/deploy.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import sys
+import yaml
+from pod_manager import Pod_Manager
+
+"""
+This file is the first executed when a booking begins.
+"""
+
+usage = """
+./deploy [--config CONFIG_FILE] [--host HOSTNAME] [--reset]
+"""
+
+
+def main(config_path, host):
+ """
+ starts the deployment with the given configuration.
+ """
+ config = yaml.safe_load(open(config_path))
+
+ manager = Pod_Manager(config, requested_host=host)
+ manager.start_deploy()
+
+
+def reset(config_path, host):
+ """
+ Tells the Pod Manager to clean and reset the given host.
+ """
+ config = yaml.safe_load(open(config_path))
+ Pod_Manager(config, requested_host=host, reset=True)
+
+
+if __name__ == "__main__":
+ # parse command line
+ host = None
+
+ if "--help" in sys.argv:
+ print usage
+ sys.exit(0)
+
+ if "--config" in sys.argv:
+ try:
+ conf = sys.argv[1+sys.argv.index("--config")]
+ open(conf)
+ except Exception:
+ print "bad config file"
+ sys.exit(1)
+ if "--host" in sys.argv:
+ try:
+ host = sys.argv[1+sys.argv.index("--host")]
+ except:
+ "host not provided. Exiting"
+ sys.exit(1)
+
+ try:
+ config_file = yaml.safe_load(open(conf))
+ except:
+ print "Failed to read from config file"
+ sys.exit(1)
+ # reset or deploy host
+ if "--reset" in sys.argv:
+ reset(conf, host)
+ else:
+ main(conf, host)
diff --git a/tools/laas-fog/source/deployment_manager.py b/tools/laas-fog/source/deployment_manager.py
new file mode 100644
index 00000000..f680fa52
--- /dev/null
+++ b/tools/laas-fog/source/deployment_manager.py
@@ -0,0 +1,108 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import logging
+from api.libvirt_api import Libvirt
+
+
+class Deployment_Manager:
+ """
+ This class manages the deployment of OPNFV on a booked host
+ if it was requested. If no OPNFV installer was requested, this class will
+ create the virtual machines and networks in the config files and exit.
+ """
+ def __init__(self, installerType, scenario, utility):
+ """
+ init function
+ """
+ # installerType will either be the constructor for an installer or None
+ self.installer = installerType
+ self.virt = Libvirt(
+ utility.host,
+ net_conf=utility.conf['hypervisor_config']['networks'],
+ dom_conf=utility.conf['hypervisor_config']['vms']
+ )
+ self.host = utility.host
+ self.util = utility
+
+ def getIso(self):
+ """
+ checks if any of the domains expect an ISO file to exist
+ and retrieves it.
+ """
+ isoDom = None
+ for dom in self.doms:
+ if dom.iso['used']:
+ isoDom = dom
+ break
+ if isoDom:
+ path = isoDom.iso['location']
+ url = isoDom.iso['URL']
+ self.util.sshExec(['wget', '-q', '-O', path, url])
+
+ def getDomMacs(self):
+ """
+ assigns the 'macs' instance variable to the domains
+ so that they know the mac addresses of their interfaces.
+ """
+ for dom in self.doms:
+ dom.macs = self.virt.getMacs(dom.name)
+
+ def makeDisks(self):
+ """
+ Creates the qcow2 disk files the domains expect on the remote host.
+ """
+ disks = []
+ for dom in self.doms:
+ disks.append(dom.disk)
+ self.util.execRemoteScript("mkDisks.sh", disks)
+
+ def go(self):
+ """
+ 'main' function.
+ creates virtual machines/networks and either passes control to the
+ OPNFV installer, or finishes up if an installer was not requested.
+ """
+ log = logging.getLogger(self.util.hostname)
+ self.virt.setLogger(log)
+ log.info("%s", "Connecting to the host hypervisor")
+ self.virt.openConnection()
+ domains, networks = self.virt.go()
+ log.info("%s", "Created all networks and VM's on host")
+ self.doms = domains
+ self.nets = networks
+ if self.installer is None:
+ log.warning("%s", "No installer requested. Finishing deployment")
+ self.util.finishDeployment()
+ return
+ log.info("%s", "retrieving ISO")
+ self.getIso()
+ self.getDomMacs()
+ self.util.copyScripts()
+ self.makeDisks()
+ log.info("%s", "Beginning installation of OPNFV")
+ try:
+ installer = self.installer(
+ self.doms,
+ self.nets,
+ self.virt,
+ self.util
+ )
+ installer.go()
+ except Exception:
+ log.exception('%s', "failed to install OPNFV")
diff --git a/tools/laas-fog/source/domain.py b/tools/laas-fog/source/domain.py
new file mode 100644
index 00000000..6f00239a
--- /dev/null
+++ b/tools/laas-fog/source/domain.py
@@ -0,0 +1,244 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import xml.dom
+import xml.dom.minidom
+import yaml
+
+
+class Domain:
+ """
+ This class defines a libvirt vm abstraction that can parse our simple
+ config file and add all necessary boiler plate and info to write a full xml
+ definition of itself for libvirt.
+ """
+
+ def __init__(self, propertiesDict):
+ """
+ init function.
+ properiesDict should be one of the dictionaries returned by the static
+ method parseConfigFile
+ """
+ self.name = propertiesDict['name']
+ self.memory = propertiesDict['memory']
+ self.vcpus = propertiesDict['vcpus']
+ self.disk = propertiesDict['disk']
+ self.iso = propertiesDict['iso']
+ # the vm will either boot from an iso or pxe
+ self.netBoot = not self.iso['used']
+ self.interfaces = propertiesDict['interfaces']
+
+ def toXML(self):
+ """
+ combines the given configuration with a lot of
+ boiler plate to create a valid libvirt xml
+ definition of a domain.
+ returns a string
+ """
+ definition = xml.dom.minidom.parseString("<domain>\n</domain>")
+ definition.documentElement.setAttribute('type', 'kvm')
+
+ nameElem = definition.createElement('name')
+ nameElem.appendChild(definition.createTextNode(self.name))
+ definition.documentElement.appendChild(nameElem)
+
+ memElem = definition.createElement('memory')
+ memElem.appendChild(definition.createTextNode(str(self.memory)))
+ definition.documentElement.appendChild(memElem)
+
+ curMemElem = definition.createElement('currentMemory')
+ curMemElem.appendChild(definition.createTextNode(str(self.memory)))
+ definition.documentElement.appendChild(curMemElem)
+
+ vcpuElem = definition.createElement('vcpu')
+ vcpuElem.appendChild(definition.createTextNode(str(self.vcpus)))
+ definition.documentElement.appendChild(vcpuElem)
+
+ osElem = definition.createElement('os')
+
+ typeElem = definition.createElement('type')
+ typeElem.setAttribute('arch', 'x86_64')
+ typeElem.appendChild(definition.createTextNode('hvm'))
+ osElem.appendChild(typeElem)
+
+ if self.netBoot:
+ bootElem = definition.createElement('boot')
+ bootElem.setAttribute('dev', 'network')
+ osElem.appendChild(bootElem)
+
+ bootElem = definition.createElement('boot')
+ bootElem.setAttribute('dev', 'hd')
+ osElem.appendChild(bootElem)
+
+ if self.iso['used']:
+ bootElem = definition.createElement('boot')
+ bootElem.setAttribute('dev', 'cdrom')
+ osElem.appendChild(bootElem)
+
+ definition.documentElement.appendChild(osElem)
+
+ featureElem = definition.createElement('feature')
+ featureElem.appendChild(definition.createElement('acpi'))
+ featureElem.appendChild(definition.createElement('apic'))
+
+ definition.documentElement.appendChild(featureElem)
+
+ cpuElem = definition.createElement('cpu')
+ cpuElem.setAttribute('mode', 'custom')
+ cpuElem.setAttribute('match', 'exact')
+ modelElem = definition.createElement('model')
+ modelElem.appendChild(definition.createTextNode('Broadwell'))
+ cpuElem.appendChild(modelElem)
+
+ definition.documentElement.appendChild(cpuElem)
+
+ clockElem = definition.createElement('clock')
+ clockElem.setAttribute('offset', 'utc')
+
+ timeElem = definition.createElement('timer')
+ timeElem.setAttribute('name', 'rtc')
+ timeElem.setAttribute('tickpolicy', 'catchup')
+ clockElem.appendChild(timeElem)
+
+ timeElem = definition.createElement('timer')
+ timeElem.setAttribute('name', 'pit')
+ timeElem.setAttribute('tickpolicy', 'delay')
+ clockElem.appendChild(timeElem)
+
+ timeElem = definition.createElement('timer')
+ timeElem.setAttribute('name', 'hpet')
+ timeElem.setAttribute('present', 'no')
+ clockElem.appendChild(timeElem)
+
+ definition.documentElement.appendChild(clockElem)
+
+ poweroffElem = definition.createElement('on_poweroff')
+ poweroffElem.appendChild(definition.createTextNode('destroy'))
+
+ definition.documentElement.appendChild(poweroffElem)
+
+ rebootElem = definition.createElement('on_reboot')
+ rebootElem.appendChild(definition.createTextNode('restart'))
+
+ definition.documentElement.appendChild(rebootElem)
+
+ crashElem = definition.createElement('on_reboot')
+ crashElem.appendChild(definition.createTextNode('restart'))
+
+ definition.documentElement.appendChild(crashElem)
+
+ pmElem = definition.createElement('pm')
+ memElem = definition.createElement('suspend-to-mem')
+ memElem.setAttribute('enabled', 'no')
+ pmElem.appendChild(memElem)
+ diskElem = definition.createElement('suspend-to-disk')
+ diskElem.setAttribute('enabled', 'no')
+ pmElem.appendChild(diskElem)
+
+ definition.documentElement.appendChild(pmElem)
+
+ deviceElem = definition.createElement('devices')
+
+ emuElem = definition.createElement('emulator')
+ emuElem.appendChild(definition.createTextNode('/usr/libexec/qemu-kvm'))
+ deviceElem.appendChild(emuElem)
+
+ diskElem = definition.createElement('disk')
+ diskElem.setAttribute('type', 'file')
+ diskElem.setAttribute('device', 'disk')
+
+ driverElem = definition.createElement('driver')
+ driverElem.setAttribute('name', 'qemu')
+ driverElem.setAttribute('type', 'qcow2')
+ diskElem.appendChild(driverElem)
+
+ sourceElem = definition.createElement('source')
+ sourceElem.setAttribute('file', self.disk)
+ diskElem.appendChild(sourceElem)
+
+ targetElem = definition.createElement('target')
+ targetElem.setAttribute('dev', 'hda')
+ targetElem.setAttribute('bus', 'ide')
+ diskElem.appendChild(targetElem)
+
+ deviceElem.appendChild(diskElem)
+
+ if self.iso['used']:
+ diskElem = definition.createElement('disk')
+ diskElem.setAttribute('type', 'file')
+ diskElem.setAttribute('device', 'cdrom')
+
+ driverElem = definition.createElement('driver')
+ driverElem.setAttribute('name', 'qemu')
+ driverElem.setAttribute('type', 'raw')
+ diskElem.appendChild(driverElem)
+
+ sourceElem = definition.createElement('source')
+ sourceElem.setAttribute('file', self.iso['location'])
+ diskElem.appendChild(sourceElem)
+
+ targetElem = definition.createElement('target')
+ targetElem.setAttribute('dev', 'hdb')
+ targetElem.setAttribute('bus', 'ide')
+ diskElem.appendChild(targetElem)
+
+ diskElem.appendChild(definition.createElement('readonly'))
+ deviceElem.appendChild(diskElem)
+
+ for iface in self.interfaces:
+ ifaceElem = definition.createElement('interface')
+ ifaceElem.setAttribute('type', iface['type'])
+ sourceElem = definition.createElement('source')
+ sourceElem.setAttribute(iface['type'], iface['name'])
+ modelElem = definition.createElement('model')
+ modelElem.setAttribute('type', 'e1000')
+ ifaceElem.appendChild(sourceElem)
+ ifaceElem.appendChild(modelElem)
+ deviceElem.appendChild(ifaceElem)
+
+ graphicElem = definition.createElement('graphics')
+ graphicElem.setAttribute('type', 'vnc')
+ graphicElem.setAttribute('port', '-1')
+ deviceElem.appendChild(graphicElem)
+
+ consoleElem = definition.createElement('console')
+ consoleElem.setAttribute('type', 'pty')
+ deviceElem.appendChild(consoleElem)
+
+ definition.documentElement.appendChild(deviceElem)
+ return definition.toprettyxml()
+
+ def writeXML(self, filePath):
+ """
+ writes this domain's xml definition to the given file.
+ """
+ f = open(filePath, 'w')
+ f.write(self.toXML())
+ f.close()
+
+ @staticmethod
+ def parseConfigFile(path):
+ """
+ parses the domains config file
+ """
+ configFile = open(path, 'r')
+ try:
+ config = yaml.safe_load(configFile)
+ except Exception:
+ print "Invalid domain configuration. exiting"
+ return config
diff --git a/tools/laas-fog/source/installers/__init__.py b/tools/laas-fog/source/installers/__init__.py
new file mode 100644
index 00000000..7bb515b7
--- /dev/null
+++ b/tools/laas-fog/source/installers/__init__.py
@@ -0,0 +1,17 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
diff --git a/tools/laas-fog/source/installers/fuel.py b/tools/laas-fog/source/installers/fuel.py
new file mode 100644
index 00000000..c5b647cf
--- /dev/null
+++ b/tools/laas-fog/source/installers/fuel.py
@@ -0,0 +1,268 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import time
+import sys
+from installer import Installer
+from api.fuel_api import Fuel_api
+
+
+class Fuel_Installer(Installer):
+ """
+ This class is the installer for any OPNFV scenarios which use Fuel as the
+ installer. This class uses the libvirt api handler
+ to create all the virtual hosts,
+ then installs fuel and uses the fuel api handler
+ to create and deploy an openstack environment
+
+ This class will get much smarter and have less configuration hardcoded
+ as we grow support for more OPNFV scenarios
+ """
+
+ def __init__(self, doms, nets, libvirt_handler, util):
+ """
+ init function
+ Calls the super constructor
+ """
+ super(Fuel_Installer, self).__init__(doms, nets, libvirt_handler, util)
+ url = 'http://'+self.libvirt.host+':8000/'
+ self.handler = Fuel_api(url, self.log, 'admin', 'admin')
+ self.fuelNets = None
+
+ def bootMaster(self):
+ """
+ Boots the fuel master node and waits
+ for it to come up
+ """
+ self.libvirt.bootMaster()
+ time.sleep(100)
+
+ def bootNodes(self):
+ """
+ Boots all the slave nodes
+ """
+ self.libvirt.bootSlaves()
+
+ def waitForNodes(self, numNodes):
+ """
+ Waits for the nodes to pxe boot and be recognized by Fuel
+ """
+ done = False
+ self.log.info("Waiting for %i nodes to boot into Fuel", numNodes)
+ discoveredNodes = 0
+ while not done:
+ discoveredNodes = len(self.handler.getNodes())
+ nodes = int(discoveredNodes)
+ self.log.info("found %d nodes", nodes)
+
+ done = discoveredNodes == numNodes
+
+ def installMaster(self):
+ """
+ runs the fuelInstall script, which uses the fuel iso to
+ install fuel onto the master node
+ """
+ self.util.execRemoteScript("ipnat.sh", [self.libvirt.host])
+ self.util.execRemoteScript("fuelInstall.sh", [self.util.remoteDir])
+
+ def makeOpenstack(self):
+ """
+ creates an openstack environment and saves
+ the openstack id
+ """
+ self.osid = self.handler.createOpenstack()
+
+ def addNodesToOpenstack(self):
+ """
+ Adds the nodes to the openstack environment with
+ compute / controller + cinder roles
+ """
+ nodesList = [
+ {"id": 1, "roles": ["controller", "cinder"]},
+ {"id": 2, "roles": ["controller", "cinder"]},
+ {"id": 3, "roles": ["controller", "cinder"]},
+ {"id": 4, "roles": ["compute"]},
+ {"id": 5, "roles": ["compute"]}
+ ]
+
+ self.handler.addNodes(self.osid, nodesList)
+
+ def configNetworks(self):
+ """
+ configures the openstack networks by calling the 3 helper
+ methods
+ """
+ self.configPublicNet()
+ self.configStorageNet()
+ self.configManagementNet()
+
+ def configPublicNet(self):
+ """
+ sets the default public network
+ changes the cidr, gateway, and floating ranges
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "public":
+ net["ip_ranges"] = [["10.20.1.10", "10.20.1.126"]]
+ net['cidr'] = "10.20.1.0/24"
+ net['gateway'] = "10.20.1.1"
+
+ # updates the floating ranges
+ rng = [["10.20.1.130", "10.20.1.254"]]
+ networks['networking_parameters']['floating_ranges'] = rng
+ self.handler.uploadNetworks(networks, self.osid)
+
+ def configStorageNet(self):
+ """
+ sets the default storage network to have the right
+ cidr and gateway, and no vlan
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "storage":
+ net["ip_ranges"] = [["10.20.3.5", "10.20.3.254"]]
+ net["cidr"] = "10.20.3.0/24"
+ net["meta"]["notation"] = "ip_ranges"
+ net["meta"]["use_gateway"] = True
+ net["gateway"] = "10.20.3.1"
+ net["vlan_start"] = None
+ self.handler.uploadNetworks(networks, self.osid)
+
+ def configManagementNet(self):
+ """
+ sets the default management net to have the right
+ cidr and gatewar and no vlan
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "management":
+ net["ip_ranges"] = [["10.20.2.5", "10.20.2.254"]]
+ net["cidr"] = "10.20.2.0/24"
+ net["meta"]["notation"] = "ip_ranges"
+ net["meta"]["use_gateway"] = True
+ net["gateway"] = "10.20.2.1"
+ net["vlan_start"] = None
+ self.handler.uploadNetworks(networks, self.osid)
+
+ # TODO: make this method smarter. I am making too many assumptions about
+ # the order of interfaces and networks
+ def configIfaces(self):
+ """
+ assigns the proper networks to each interface of the nodes
+ """
+ for x in range(1, 6):
+ idNum = x
+ ifaceJson = self.handler.getIfaces(idNum)
+
+ ifaceJson[0]['assigned_networks'] = [
+ {"id": 1, "name": "fuelweb_admin"},
+ {"id": 5, "name": "private"}
+ ]
+ ifaceJson[2]['assigned_networks'] = [
+ {"id": 4, "name": "storage"}
+ ]
+ ifaceJson[3]['assigned_networks'] = [
+ {"id": 3, "name": "management"}
+ ]
+ if idNum < 4:
+ ifaceJson[1]['assigned_networks'] = [{
+ "id": 2,
+ "name": "pubic"
+ }]
+
+ self.handler.setIfaces(idNum, ifaceJson)
+
+ def clearAdminIface(self, ifaceJson, node):
+ """
+ makes the admin interface have *only* the admin network
+ assigned to it
+ """
+ for iface in ifaceJson:
+ if iface['mac'] == node.macs['admin']:
+ iface['assigned_networks'] = [{
+ "id": 1,
+ "name": "fuelweb_admin"
+ }]
+
+ def deployOpenstack(self):
+ """
+ Once openstack is properly configured, this method
+ deploy OS and returns when OS is running
+ """
+ self.log.info("%s", "Deploying Openstack environment.")
+ self.log.info("%s", "This may take a while")
+ self.handler.deployOpenstack(self.osid)
+
+ def getKey(self):
+ """
+ Retrieves authentication tokens for the api handler,
+ while allowing the first few attempts to fail to
+ allow Fuel time to "wake up"
+ """
+ i = 0
+ while i < 20:
+ i += 1
+ try:
+ self.handler.getKey()
+ return
+ except Exception:
+ self.log.warning("%s", "Failed to talk to Fuel api")
+ self.log.warning("Exec try %d/20", i)
+ try:
+ self.handler.getKey()
+ except Exception:
+ self.logger.exception("%s", "Fuel api is unavailable")
+ sys.exit(1)
+
+ def go(self):
+ """
+ This method does all the work of this class.
+ It installs the master node, boots the slaves
+ into Fuel, creates and configures OS, and then
+ deploys it and uses NAT to make the horizon dashboard
+ reachable
+ """
+ self.libvirt.openConnection()
+ self.log.info('%s', 'installing the Fuel master node.')
+ self.log.info('%s', 'This will take some time.')
+ self.installMaster()
+ time.sleep(60)
+ self.getKey()
+ self.log.info('%s', 'The master node is installed.')
+ self.log.info('%s', 'Waiting for bootstrap image to build')
+ self.handler.waitForBootstrap()
+ self.bootNodes()
+ self.waitForNodes(5)
+ self.log.info('%s', "Defining an openstack environment")
+ self.makeOpenstack()
+ self.addNodesToOpenstack()
+ self.log.info('%s', "configuring interfaces...")
+ self.configIfaces()
+ self.log.info('%s', "configuring networks...")
+ self.configNetworks()
+ self.deployOpenstack()
+
+ horizon = self.handler.getHorizonIP(self.osid)
+ self.util.execRemoteScript(
+ '/horizonNat.sh', [self.libvirt.host, horizon])
+ notice = "You may access the Openstack dashboard at %s/horizon"
+ self.log.info(notice, self.libvirt.host)
+
+ self.libvirt.close()
+ self.util.finishDeployment()
diff --git a/tools/laas-fog/source/installers/installer.py b/tools/laas-fog/source/installers/installer.py
new file mode 100644
index 00000000..d4c4889f
--- /dev/null
+++ b/tools/laas-fog/source/installers/installer.py
@@ -0,0 +1,35 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+
+class Installer(object):
+ """
+ This is a simple base class to define a single constructor
+ for all the different installer types.
+ I may move more functionality to this class as we add support for more
+ installers and there becomes common fucntions that would be nice to share
+ between installers.
+ """
+
+ def __init__(self, domList, netList, libvirt_handler, util):
+ self.doms = domList
+ self.nets = netList
+ self.libvirt = libvirt_handler
+ self.osid = 0
+ self.util = util
+ self.log = util.createLogger(util.hostname)
diff --git a/tools/laas-fog/source/installers/joid.py b/tools/laas-fog/source/installers/joid.py
new file mode 100644
index 00000000..a3f3bcf1
--- /dev/null
+++ b/tools/laas-fog/source/installers/joid.py
@@ -0,0 +1,40 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+"""
+This class will install Joid onto the remote host.
+Currently only supports joid's "default" configuration
+"""
+
+
+class Joid_Installer:
+
+ def __init__(self, doms, nets, libvirt_handler, util):
+ """
+ init function calls the super constructor
+ """
+ super(Joid_Installer, self).__init__(doms, nets, libvirt_handler, util)
+
+ def go(self):
+ """
+ does all the work of this class.
+ Currently just runs the joidInstall script, which installs joid
+ onto the remote host
+ """
+ self.logger.info("%s", "Executing joid virtual installation")
+ self.util.execRemoteScript("joidInstall.sh")
diff --git a/tools/laas-fog/source/listen.py b/tools/laas-fog/source/listen.py
new file mode 100755
index 00000000..ed714c9a
--- /dev/null
+++ b/tools/laas-fog/source/listen.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+import subprocess
+import sys
+import os
+import yaml
+
+"""
+This is the file that the user will execute to start the whole process.
+This file will start the pharos api listener in a new process and then exit.
+"""
+
+
+def checkArgs():
+ """
+ error checks the cmd line args and gets the path
+ of the config file
+ """
+ usage = "./listen.py --config <path_to_pharos_config>"
+ if "--help" in sys.argv:
+ print usage
+ sys.exit(0)
+
+ if "--config" not in sys.argv:
+ print usage
+ sys.exit(1)
+
+ try:
+ i = sys.argv.index("--config")
+ config_file = sys.argv[i+1]
+ # verifies that the file exists, is readable, and formatted correctly
+ yaml.safe_load(open(config_file))
+ return config_file
+ except Exception:
+ print "Bad config file"
+ sys.exit(1)
+
+
+# reads args and starts the pharos listener in the background
+config = checkArgs()
+source_dir = os.path.dirname(os.path.realpath(__file__))
+pharos_path = os.path.join(source_dir, "pharos.py")
+subprocess.Popen(['/usr/bin/python', pharos_path, '--config', config])
diff --git a/tools/laas-fog/source/network.py b/tools/laas-fog/source/network.py
new file mode 100644
index 00000000..234ba22e
--- /dev/null
+++ b/tools/laas-fog/source/network.py
@@ -0,0 +1,103 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import sys
+import xml.dom
+import xml.dom.minidom
+import yaml
+
+
+class Network:
+ """
+ This class has a similar role as the Domain class.
+ This class will parse a config file and
+ write the xml definitions of those networks for libvirt.
+ """
+
+ def __init__(self, propertiesDict):
+ """
+ init. propertiesDict should be
+ one of the dictionaries returned by parseConfigFile
+ """
+ self.name = propertiesDict['name']
+ self.brName = propertiesDict['brName']
+ self.brAddr = propertiesDict['brAddr']
+ self.netmask = propertiesDict['netmask']
+ self.forward = propertiesDict['forward']
+ self.dhcp = propertiesDict['dhcp']
+ self.cidr = propertiesDict['cidr']
+
+ def toXML(self):
+ """
+ Takes the config of this network and writes a valid xml definition
+ for libvirt.
+ returns a string
+ """
+ definition = xml.dom.minidom.parseString("<network>\n</network>")
+ nameElem = definition.createElement('name')
+ nameElem.appendChild(definition.createTextNode(self.name))
+ definition.documentElement.appendChild(nameElem)
+
+ if self.forward['used']:
+ forwardElem = definition.createElement('forward')
+ forwardElem.setAttribute('mode', self.forward['type'])
+ definition.documentElement.appendChild(forwardElem)
+
+ bridgeElem = definition.createElement('bridge')
+ bridgeElem.setAttribute('name', self.brName)
+ bridgeElem.setAttribute('stp', 'on')
+ bridgeElem.setAttribute('delay', '5')
+ definition.documentElement.appendChild(bridgeElem)
+
+ ipElem = definition.createElement('ip')
+ ipElem.setAttribute('address', self.brAddr)
+ ipElem.setAttribute('netmask', self.netmask)
+ if self.dhcp['used']:
+ dhcpElem = definition.createElement('dhcp')
+ rangeElem = definition.createElement('range')
+ rangeElem.setAttribute('start', self.dhcp['rangeStart'])
+ rangeElem.setAttribute('end', self.dhcp['rangeEnd'])
+ dhcpElem.appendChild(rangeElem)
+ ipElem.appendChild(dhcpElem)
+
+ definition.documentElement.appendChild(ipElem)
+
+ self.xml = definition.toprettyxml()
+ return self.xml
+
+ def writeXML(self, filePath):
+ """
+ writes xml definition to given file
+ """
+ f = open(filePath, 'w')
+ f.write(self.toXML())
+ f.close()
+
+ @staticmethod
+ def parseConfigFile(path):
+ """
+ parses given config file
+ """
+ configFile = open(path, 'r')
+ try:
+ config = yaml.safe_load(configFile)
+ except Exception:
+ print "Bad network configuration file. exiting"
+ sys.exit(1)
+
+ return config
diff --git a/tools/laas-fog/source/pharos.py b/tools/laas-fog/source/pharos.py
new file mode 100755
index 00000000..d5a6e8a8
--- /dev/null
+++ b/tools/laas-fog/source/pharos.py
@@ -0,0 +1,217 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import requests
+import time
+import calendar
+import subprocess
+import sys
+import yaml
+import os
+import logging
+from utilities import Utilities
+from database import BookingDataBase
+
+
+class Pharos_api:
+ """
+ This class listens to the dashboard and starts/stops bookings accordingly.
+ This class should run in the background indefinitely.
+ Do not execute this file directly - run ./listen.py instead
+ """
+ def __init__(self, config):
+ """
+ init function.
+ config is the already-parsed config file
+ """
+ self.conf = config
+ self.servers = yaml.safe_load(open(config['inventory']))
+ self.log = self.createLogger("pharos_api")
+ self.polling = 60 / int(config['polling'])
+ self.log.info(
+ "polling the dashboard once every %d seconds", self.polling)
+ self.dashboard = config['dashboard']
+ self.log.info("connecting to dashboard at %s", self.dashboard)
+ if os.path.isfile(config['token']):
+ self.token = open(config['token']).read()
+ else:
+ self.token = config['token']
+ self.updateHeader()
+ self.database = BookingDataBase(config['database'])
+ self.log.info("using database at %s", self.conf['database'])
+ self.deploy_path = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "deploy.py")
+ if not os.path.isfile(self.deploy_path):
+ self.log.error(
+ "Cannot find the deployment script at %s", self.deploy_path)
+
+ def setToken(self, token):
+ """
+ Sets authentication token. Not yet needed.
+ """
+ self.token = token
+ self.updateHeader()
+
+ def setTokenFromFile(self, path):
+ """
+ reads auth token from a file. Not yet needed.
+ """
+ self.setToken(open(path).read())
+
+ def updateHeader(self):
+ """
+ updates the http header used when talking to the dashboard
+ """
+ self.header = {"Authorization": "Token " + self.token}
+
+ def listen(self):
+ """
+ this method will continuously poll the pharos dashboard.
+ If a booking is found on our server,
+ we will start a deployment in the background with the
+ proper config file for the requested
+ installer and scenario.
+ """
+ self.log.info("%s", "Beginning polling of dashboard")
+ try:
+ while True:
+ time.sleep(self.polling)
+ url = self.dashboard+"/api/bookings/"
+ bookings = requests.get(url, headers=self.header).json()
+ for booking in bookings:
+ if booking['resource_id'] in self.servers.keys():
+ self.convertTimes(booking)
+ self.database.checkAddBooking(booking)
+ self.checkBookings()
+ except Exception:
+ self.log.exception('%s', "failed to connect to dashboard")
+
+ self.listen()
+
+ def convertTimes(self, booking):
+ """
+ this method will take the time reported by Pharos in the
+ format yyyy-mm-ddThh:mm:ssZ
+ and convert it into seconds since the epoch,
+ for easier management
+ """
+ booking['start'] = self.pharosToEpoch(booking['start'])
+ booking['end'] = self.pharosToEpoch(booking['end'])
+
+ def pharosToEpoch(self, timeStr):
+ """
+ Converts the dates from the dashboard to epoch time.
+ """
+ time_struct = time.strptime(timeStr, '%Y-%m-%dT%H:%M:%SZ')
+ epoch_time = calendar.timegm(time_struct)
+ return epoch_time
+
+ def checkBookings(self):
+ """
+ This method checks all the bookings in our database to see if any
+ action is required.
+ """
+ # get all active bookings from database into a usable form
+ bookings = self.database.getBookings()
+ for booking in bookings:
+ # first, check if booking is over
+ if time.time() > booking[3]:
+ self.log.info("ending the booking with id %i", booking[0])
+ self.endBooking(booking)
+ # Then check if booking has begun and the host is still idle
+ elif time.time() > booking[2] and booking[7] < 1:
+ self.log.info("starting the booking with id %i", booking[0])
+ self.startBooking(booking)
+
+ def startBooking(self, booking):
+ """
+ Starts the scheduled booking on the requested host with
+ the correct config file.
+ The provisioning process gets spun up in a subproccess,
+ so the api listener is not interupted.
+ """
+ try:
+ host = self.servers[booking[1]]
+ self.log.info("Detected a new booking started for host %s", host)
+ config_file = self.conf['default_configs']["None"]
+ try:
+ config_file = self.conf['default_configs'][booking[4]]
+ except KeyError:
+ self.log.warning(
+ "No installer detected in the booking request.")
+ self.log.info("New booking started for host %s", host)
+ self.database.setStatus(booking[0], 1) # mark booking started
+ if not os.path.isfile(self.deploy_path):
+ error = "Cannot find the deploment script at %s"
+ self.log.error(error, self.deploy_path)
+ subprocess.Popen([
+ '/usr/bin/python',
+ self.deploy_path,
+ '--config', config_file,
+ '--host', host
+ ])
+ except Exception:
+ self.log.exception("Failed to start booking for %s", host)
+
+ def endBooking(self, booking):
+ """
+ Resets a host once its booking has ended.
+ """
+ try:
+ try:
+ config_file = self.conf['default_configs'][booking[4]]
+ except KeyError:
+ warn = "No installer detected in booking request"
+ self.log.warning("%s", warn)
+ config_file = self.conf['default_configs']["None"]
+
+ host = self.servers[booking[1]]
+ log = logging.getLogger(host)
+ log.info('Lease expired. Resetting host %s', host)
+ self.database.setStatus(booking[0], 3)
+ if not os.path.isfile(self.deploy_path):
+ err = "Cannot find deployment script at %s"
+ self.log.error(err, self.deploy_path)
+ subprocess.Popen([
+ '/usr/bin/python',
+ self.deploy_path,
+ '--config', config_file,
+ '--host', host,
+ '--reset'
+ ])
+ self.database.removeBooking(booking[0])
+ except Exception:
+ self.log.exception("Failed to end booking for %s", host)
+
+ def createLogger(self, name):
+ return Utilities.createLogger(name, self.conf['logging_dir'])
+
+
+if __name__ == "__main__":
+ if "--config" not in sys.argv:
+ print "Specify config file with --config option"
+ sys.exit(1)
+ config = None
+ try:
+ config_file = sys.argv[1+sys.argv.index('--config')]
+ config = yaml.safe_load(open(config_file))
+ except Exception:
+ sys.exit(1)
+ api = Pharos_api(config)
+ api.listen()
diff --git a/tools/laas-fog/source/pod_manager.py b/tools/laas-fog/source/pod_manager.py
new file mode 100755
index 00000000..3e1caa8e
--- /dev/null
+++ b/tools/laas-fog/source/pod_manager.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import time
+import sys
+import yaml
+import os
+from api.fog import FOG_Handler
+from utilities import Utilities
+from deployment_manager import Deployment_Manager
+from database import HostDataBase
+from installers import fuel
+from installers import joid
+
+
+class Pod_Manager:
+ """
+ This is the 'main' class that chooses a host and provisions & deploys it.
+ this class can be run directly from the command line,
+ or it can be called from the pharos dashboard listener when
+ a deployment is requested.
+ Either way, this file should be called with:
+ ./pod_manager.py --config <CONFIG_FILE>
+ """
+ # This dictionary allows me to map the supported installers to the
+ # respective installer classes, for easier parsing of the config file
+ INSTALLERS = {
+ "fuel": fuel.Fuel_Installer,
+ "joid": joid.Joid_Installer,
+ "none": None
+ }
+
+ def __init__(self, conf, requested_host=None, reset=False):
+ """
+ init function.
+ conf is the read and parsed config file for this deployment
+ requested_host is the optional hostname of the host you request
+ if reset, we just flash the host to a clean state and return.
+ """
+ self.conf = conf
+ if self.conf['installer'] is not None:
+ inst = Pod_Manager.INSTALLERS[self.conf['installer'].lower()]
+ self.conf['installer'] = inst
+ self.fog = FOG_Handler(self.conf['fog']['server'])
+ # Sets the fog keys, either from the config file
+ # or the secrets file the config points to
+ if os.path.isfile(self.conf['fog']['api_key']):
+ self.fog.getFogKeyFromFile(self.conf['fog']['api_key'])
+ else:
+ self.fog.setFogKey(self.conf['fog']['api_key'])
+
+ if os.path.isfile(self.conf['fog']['user_key']):
+ self.fog.getUserKeyFromFile(self.conf['fog']['user_key'])
+ else:
+ self.fog.setUserKey(self.conf['fog']['user_key'])
+ self.database = HostDataBase(self.conf['database'])
+ self.request = requested_host
+ if reset:
+ mac = self.fog.getHostMac(self.request)
+ log = self.conf['dhcp_log']
+ dhcp_serv = self.conf['dhcp_server']
+ ip = Utilities.getIPfromMAC(mac, log, remote=dhcp_serv)
+ self.flash_host(self.request, ip)
+
+ def start_deploy(self):
+ """
+ Ghosts the machine with the proper disk image and hands off
+ control to the deployment manager.
+ """
+ try:
+ host = self.database.getHost(self.request)
+ hostMac = self.fog.getHostMac(host)
+ dhcp_log = self.conf['dhcp_log']
+ dhcp_server = self.conf['dhcp_server']
+ host_ip = Utilities.getIPfromMAC(
+ hostMac, dhcp_log, remote=dhcp_server
+ )
+ util = Utilities(host_ip, host, self.conf)
+ util.resetKnownHosts()
+ log = Utilities.createLogger(host, self.conf['logging_dir'])
+ self.fog.setLogger(log)
+ log.info("Starting booking on host %s", host)
+ log.info("host is reachable at %s", host_ip)
+ log.info('ghosting host %s with clean image', host)
+ self.flash_host(host, host_ip, util)
+ log.info('Host %s imaging complete', host)
+ inst = self.conf['installer']
+ scenario = self.conf['scenario']
+ Deployment_Manager(inst, scenario, util).go()
+ except Exception:
+ log.exception("Encountered an unexpected error")
+
+ def flash_host(self, host, host_ip, util=None):
+ """
+ We do this using a FOG server, but you can use whatever fits into your
+ lab infrastructure. This method should put the host into a state as if
+ centos was just freshly installed, updated,
+ and needed virtualization software installed.
+ This is the 'clean' starting point we work from
+ """
+ self.fog.setImage(host, self.conf['fog']['image_id'])
+ self.fog.imageHost(host)
+ Utilities.restartRemoteHost(host_ip)
+ self.fog.waitForHost(host)
+ # if util is not given, then we are just
+ # flashing to reset after a booking expires
+ if util is not None:
+ time.sleep(30)
+ util.waitForBoot()
+ util.checkHost()
+ time.sleep(15)
+ util.checkHost()
+
+
+if __name__ == "__main__":
+ configFile = ""
+ host = ""
+ for i in range(len(sys.argv) - 1):
+ if "--config" in sys.argv[i]:
+ configFile = sys.argv[i+1]
+ elif "--host" in sys.argv[i]:
+ host = sys.argv[i+1]
+ if len(configFile) < 1:
+ print "No config file specified"
+ sys.exit(1)
+ configFile = yaml.safe_load(open(configFile))
+ manager = Pod_Manager(configFile, requested_host=host)
+ manager.start_deploy()
diff --git a/tools/laas-fog/source/resetDataBase.py b/tools/laas-fog/source/resetDataBase.py
new file mode 100755
index 00000000..ff141e58
--- /dev/null
+++ b/tools/laas-fog/source/resetDataBase.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import sys
+import os
+import yaml
+from api.fog import FOG_Handler
+from database import HostDataBase
+from database import BookingDataBase
+
+"""
+This file just resets the host database with
+all the hosts in fog, with all of them
+showing as available
+
+This file is just provided to make populating the host db easier.
+If you wanted to do this yourself, you could do the following in
+a python command prompt:
+ from database import HostDataBase
+ db = HostDataBase("/path/to/file")
+ db.addHost("host-name")
+ db.addHost("host-name")
+ db.addHost("host-name")
+
+"""
+config = None
+if "--config" in sys.argv:
+ i = sys.argv.index("--config")
+ if len(sys.argv) > i+1 and os.path.isfile(sys.argv[i+1]):
+ try:
+ config = yaml.safe_load(open(sys.argv[i+1]))
+ except Exception:
+ print "failed to read config file. exiting"
+ sys.exit(1)
+ else:
+ print "config file not found. exiting"
+ sys.exit(1)
+else:
+ print "no config file given. Specify file with '--config <FILE_PATH>'"
+ sys.exit(1)
+
+host = False
+if "--host" in sys.argv or "--both" in sys.argv:
+ host = True
+
+booking = False
+if "--booking" in sys.argv or "--both" in sys.argv:
+ booking = True
+
+
+if host:
+
+ fog = FOG_Handler(
+ config['fog']['server']
+ )
+ if os.path.isfile(config['fog']['api_key']):
+ fog.getFogKeyFromFile(config['fog']['api_key'])
+ else:
+ fog.setFogKey(config['fog']['api_key'])
+
+ if os.path.isfile(config['fog']['user_key']):
+ fog.getUserKeyFromFile(config['fog']['user_key'])
+ else:
+ fog.setUserKey(config['fog']['user_key'])
+ hosts = fog.getHostsinGroup("vm")
+ host_names = []
+ for host in hosts:
+ host_names.append(host['name'])
+
+ # creates the directory of the db, if it doesnt yet exist
+ dbDir = os.path.dirname(config['database'])
+ if not os.path.isdir(dbDir):
+ os.makedirs(dbDir)
+
+ db = HostDataBase(config['database'])
+
+ # check if the table already exists or not
+ try:
+ db.cursor.execute("SELECT * FROM hosts")
+ except Exception as err:
+ if "no such table" in str(err):
+ db.createTable()
+
+ db.resetHosts(host_names)
+
+if booking:
+ db = BookingDataBase(config['database'])
+ db.createTable()
+ db.close()
+
+else:
+ print "you must specify the '--host', '--booking', or '--both' option"
+ print "depending on which database you wish to reset"
+ sys.exit(0)
diff --git a/tools/laas-fog/source/stop.sh b/tools/laas-fog/source/stop.sh
new file mode 100755
index 00000000..e7214829
--- /dev/null
+++ b/tools/laas-fog/source/stop.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+
+
+# This just finds all processes from this program and kills them.
+
+
+PIDS=$(ps -ef | grep laas/source/ | grep python | awk '{print $2}')
+
+kill ${PIDS[*]}
diff --git a/tools/laas-fog/source/utilities.py b/tools/laas-fog/source/utilities.py
new file mode 100644
index 00000000..bbe09467
--- /dev/null
+++ b/tools/laas-fog/source/utilities.py
@@ -0,0 +1,346 @@
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#Licensed under the Apache License, Version 2.0 (the "License"); #
+#you may not use this file except in compliance with the License. #
+#You may obtain a copy of the License at #
+# #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+# #
+#Unless required by applicable law or agreed to in writing, software #
+#distributed under the License is distributed on an "AS IS" BASIS, #
+#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
+#See the License for the specific language governing permissions and #
+#limitations under the License. #
+#############################################################################
+"""
+
+import os
+import logging
+import string
+import sys
+import subprocess
+import xml.dom
+import xml.dom.minidom
+import re
+import random
+import yaml
+from database import HostDataBase, BookingDataBase
+from api.vpn import VPN
+LOGGING_DIR = ""
+
+
+class Utilities:
+ """
+ This class defines some useful functions that may be needed
+ throughout the provisioning and deployment stage.
+ The utility object is carried through most of the deployment process.
+ """
+ def __init__(self, host_ip, hostname, conf):
+ """
+ init function
+ host_ip is the ip of the target host
+ hostname is the FOG hostname of the host
+ conf is the parsed config file
+ """
+ self.host = host_ip
+ self.hostname = hostname
+ root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+ self.scripts = os.path.join(root_dir, "hostScripts/")
+ self.remoteDir = "/root/hostScripts/"
+ self.conf = conf
+ self.logger = logging.getLogger(hostname)
+
+ def execRemoteScript(self, script, args=[]):
+ """
+ executes the given script on the
+ remote host with the given args.
+ script must be found in laas/hostScripts
+ """
+ cmd = [self.remoteDir+script]
+ for arg in args:
+ cmd.append(arg)
+ self.sshExec(cmd)
+
+ def waitForBoot(self):
+ """
+ Continually pings the host, waiting for it to boot
+ """
+ i = 0
+ while (not self.pingHost()) and i < 30:
+ i += 1
+ if i == 30:
+ self.logger.error("Host %s has not booted", self.host)
+ sys.exit(1)
+
+ def checkHost(self):
+ """
+ returns true if the host responds to two pings.
+ Sometimes, while a host is pxe booting, a host will
+ respond to one ping but quickly go back offline.
+ """
+ if self.pingHost() and self.pingHost():
+ return True
+ return False
+
+ def pingHost(self):
+ """
+ returns true if the host responds to a ping
+ """
+ i = 0
+ response = 1
+ cmd = "ping -c 1 "+self.host
+ cmd = cmd.split(' ')
+ nul = open(os.devnull, 'w')
+ while i < 10 and response != 0:
+ response = subprocess.call(cmd, stdout=nul, stderr=nul)
+ i = i + 1
+ if response == 0:
+ return True
+ return False
+
+ def copyDir(self, localDir, remoteDir):
+ """
+ uses scp to copy localDir to remoteDir on the
+ remote host
+ """
+ cmd = "mkdir -p "+remoteDir
+ self.sshExec(cmd.split(" "))
+ cmd = "scp -o StrictHostKeyChecking=no -r "
+ cmd += localDir+" root@"+self.host+":/root"
+ cmd = cmd.split()
+ nul = open(os.devnull, 'w')
+ subprocess.call(cmd, stdout=nul, stderr=nul)
+
+ def copyScripts(self):
+ """
+ Copies the hostScrpts dir to the remote host.
+ """
+ self.copyDir(self.scripts, self.remoteDir)
+
+ def sshExec(self, args):
+ """
+ executes args as an ssh
+ command on the remote host.
+ """
+ cmd = ['ssh', 'root@'+self.host]
+ for arg in args:
+ cmd.append(arg)
+ nul = open(os.devnull, 'w')
+ return subprocess.call(cmd, stdout=nul, stderr=nul)
+
+ def resetKnownHosts(self):
+ """
+ edits your known hosts file to remove the previous entry of host
+ Sometimes, the flashing process gives the remote host a new
+ signature, and ssh complains about it.
+ """
+ lines = []
+ sshFile = open('/root/.ssh/known_hosts', 'r')
+ lines = sshFile.read()
+ sshFile.close()
+ lines = lines.split('\n')
+ sshFile = open('/root/.ssh/known_hosts', 'w')
+ for line in lines:
+ if self.host not in line:
+ sshFile.write(line+'\n')
+ sshFile.close()
+
+ def restartHost(self):
+ """
+ restarts the remote host
+ """
+ cmd = ['shutdown', '-r', 'now']
+ self.sshExec(cmd)
+
+ @staticmethod
+ def randoString(length):
+ """
+ this is an adapted version of the code found here:
+ https://stackoverflow.com/questions/2257441/
+ random-string-generation-with-upper-case-letters-and-digits-in-python
+ generates a random alphanumeric string of length length.
+ """
+ randStr = ''
+ chars = string.ascii_uppercase + string.digits
+ for x in range(length):
+ randStr += random.SystemRandom().choice(chars)
+ return randStr
+
+ def changePassword(self):
+ """
+ Sets the root password to a random string and returns it
+ """
+ paswd = self.randoString(15)
+ command = "printf "+paswd+" | passwd --stdin root"
+ self.sshExec(command.split(' '))
+ return paswd
+
+ def markHostDeployed(self):
+ """
+ Tells the database that this host has finished its deployment
+ """
+ db = HostDataBase(self.conf['database'])
+ db.makeHostDeployed(self.hostname)
+ db.close()
+
+ def make_vpn_user(self):
+ """
+ Creates a vpn user and associates it with this booking
+ """
+ config = yaml.safe_load(open(self.conf['vpn_config']))
+ myVpn = VPN(config)
+ # name = dashboard.getUserName()
+ u, p, uid = myVpn.makeNewUser() # may pass name arg if wanted
+ self.logger.info("%s", "created new vpn user")
+ self.logger.info("username: %s", u)
+ self.logger.info("password: %s", p)
+ self.logger.info("vpn user uid: %s", uid)
+ self.add_vpn_user(uid)
+
+ def add_vpn_user(self, uid):
+ """
+ Adds the dn of the vpn user to the database
+ so that we can clean it once the booking ends
+ """
+ db = BookingDataBase(self.conf['database'])
+ # converts from hostname to pharos resource id
+ inventory = yaml.safe_load(open(self.conf['inventory']))
+ host_id = -1
+ for resource_id in inventory.keys():
+ if inventory[resource_id] == self.hostname:
+ host_id = resource_id
+ break
+ db.setVPN(host_id, uid)
+
+ def finishDeployment(self):
+ """
+ Last method call once a host is finished being deployed.
+ It notifies the database and changes the password to
+ a random string
+ """
+ self.markHostDeployed()
+ self.make_vpn_user()
+ passwd = self.changePassword()
+ self.logger.info("host %s provisioning done", self.hostname)
+ self.logger.info("You may access the host at %s", self.host)
+ self.logger.info("The password is %s", passwd)
+ notice = "You should change all passwords for security"
+ self.logger.warning('%s', notice)
+
+ @staticmethod
+ def restartRemoteHost(host_ip):
+ """
+ This method assumes that you already have ssh access to the target
+ """
+ nul = open(os.devnull, 'w')
+ ret_code = subprocess.call([
+ 'ssh', '-o', 'StrictHostKeyChecking=no',
+ 'root@'+host_ip,
+ 'shutdown', '-r', 'now'],
+ stdout=nul, stderr=nul)
+
+ return ret_code
+
+ @staticmethod
+ def getName(xmlString):
+ """
+ Gets the name value from xml. for example:
+ <name>Parker</name> returns Parker
+ """
+ xmlDoc = xml.dom.minidom.parseString(xmlString)
+ nameNode = xmlDoc.documentElement.getElementsByTagName('name')
+ name = str(nameNode[0].firstChild.nodeValue)
+ return name
+
+ @staticmethod
+ def getXMLFiles(directory):
+ """
+ searches directory non-recursively and
+ returns a list of all xml files
+ """
+ contents = os.listdir(directory)
+ fileContents = []
+ for item in contents:
+ if os.path.isfile(os.path.join(directory, item)):
+ fileContents.append(os.path.join(directory, item))
+ xmlFiles = []
+ for item in fileContents:
+ if 'xml' in os.path.basename(item):
+ xmlFiles.append(item)
+ return xmlFiles
+
+ @staticmethod
+ def createLogger(name, log_dir=LOGGING_DIR):
+ """
+ Initializes the logger if it does not yet exist, and returns it.
+ Because of how python logging works, calling logging.getLogger()
+ with the same name always returns a reference to the same log file.
+ So we can call this method from anywhere with the hostname as
+ the name arguement and it will return the log file for that host.
+ The formatting includes the level of importance and the time stamp
+ """
+ global LOGGING_DIR
+ if log_dir != LOGGING_DIR:
+ LOGGING_DIR = log_dir
+ log = logging.getLogger(name)
+ if len(log.handlers) > 0: # if this logger is already initialized
+ return log
+ log.setLevel(10)
+ han = logging.FileHandler(os.path.join(log_dir, name+".log"))
+ han.setLevel(10)
+ log_format = '[%(levelname)s] %(asctime)s [#] %(message)s'
+ formatter = logging.Formatter(fmt=log_format)
+ han.setFormatter(formatter)
+ log.addHandler(han)
+ return log
+
+ @staticmethod
+ def getIPfromMAC(macAddr, logFile, remote=None):
+ """
+ searches through the dhcp logs for the given mac
+ and returns the associated ip. Will retrieve the
+ logFile from a remote host if remote is given.
+ if given, remote should be an ip address or hostname that
+ we can ssh to.
+ """
+ if remote is not None:
+ logFile = Utilities.retrieveFile(remote, logFile)
+ ip = Utilities.getIPfromLog(macAddr, logFile)
+ if remote is not None:
+ os.remove(logFile)
+ return ip
+
+ @staticmethod
+ def retrieveFile(host, remote_loc, local_loc=os.getcwd()):
+ """
+ Retrieves file from host and puts it in the current directory
+ unless local_loc is given.
+ """
+ subprocess.call(['scp', 'root@'+host+':'+remote_loc, local_loc])
+ return os.path.join(local_loc, os.path.basename(remote_loc))
+
+ @staticmethod
+ def getIPfromLog(macAddr, logFile):
+ """
+ Helper method for getIPfromMAC.
+ uses regex to find the ip address in the
+ log
+ """
+ try:
+ messagesFile = open(logFile, "r")
+ allLines = messagesFile.readlines()
+ except Exception:
+ sys.exit(1)
+ importantLines = []
+ for line in allLines:
+ if macAddr in line and "DHCPACK" in line:
+ importantLines.append(line)
+ ipRegex = r'(\d+\.\d+\.\d+\.\d+)'
+ IPs = []
+ for line in importantLines:
+ IPs.append(re.findall(ipRegex, line))
+ if len(IPs) > 0 and len(IPs[-1]) > 0:
+ return IPs[-1][0]
+ return None