Adds the Free and Open-source Ghost (FOG) 61/39761/3
authorParker Berberian <pberberian@iol.unh.edu>
Mon, 21 Aug 2017 13:03:53 +0000 (09:03 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Thu, 31 Aug 2017 17:28:57 +0000 (13:28 -0400)
JIRA: N/A

Adds a handler which will talk with the REST api
running on the FOG server.
This allows the pod_manager to ghost images onto hosts
in order to prep them for deployment and clean them afterwards.

Change-Id: Ic00e992874ca3371b6d6e8ac2450a1ef0c765e67
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
tools/laas-fog/source/api/__init__.py [new file with mode: 0644]
tools/laas-fog/source/api/fog.py [new file with mode: 0644]

diff --git a/tools/laas-fog/source/api/__init__.py b/tools/laas-fog/source/api/__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/api/fog.py b/tools/laas-fog/source/api/fog.py
new file mode 100644 (file)
index 0000000..6287403
--- /dev/null
@@ -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