Adds Fuel Installation Capability 71/39771/2
authorParker Berberian <pberberian@iol.unh.edu>
Mon, 21 Aug 2017 13:24:57 +0000 (09:24 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Thu, 31 Aug 2017 17:28:58 +0000 (13:28 -0400)
JIRA: N/A

Adds the ability to automatically install and deploy Fuel onto
a remote host.
the hostScripts/fuelInstall.sh script boots the master machine
and uses the fuel iso to install fuel to the machine.
the source/installers/fuel.py then uses the source/api/fuel_api
handler to configure and deploy fuel.
Running:
    Fuel_Installer(domains,networks,libvirt,utility).go()
Will install fuel and deploy Openstack on top of Fuel
assuming the host is properly configured.

Change-Id: I41aee773b27b893311c945221b93eacf36aa83cc
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
tools/laas-fog/hostScripts/fuelInstall.sh [new file with mode: 0755]
tools/laas-fog/hostScripts/horizonNat.sh [new file with mode: 0755]
tools/laas-fog/source/api/fuel_api.py [new file with mode: 0644]
tools/laas-fog/source/installers/__init__.py [new file with mode: 0644]
tools/laas-fog/source/installers/fuel.py [new file with mode: 0644]
tools/laas-fog/source/installers/installer.py [new file with mode: 0644]

diff --git a/tools/laas-fog/hostScripts/fuelInstall.sh b/tools/laas-fog/hostScripts/fuelInstall.sh
new file mode 100755 (executable)
index 0000000..c68907d
--- /dev/null
@@ -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 (executable)
index 0000000..dd6396c
--- /dev/null
@@ -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/source/api/fuel_api.py b/tools/laas-fog/source/api/fuel_api.py
new file mode 100644 (file)
index 0000000..0127800
--- /dev/null
@@ -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/installers/__init__.py b/tools/laas-fog/source/installers/__init__.py
new file mode 100644 (file)
index 0000000..7bb515b
--- /dev/null
@@ -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 (file)
index 0000000..c5b647c
--- /dev/null
@@ -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 (file)
index 0000000..d4c4889
--- /dev/null
@@ -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)