Fixes to server
[ta/remote-installer.git] / src / remoteinstaller / server / server.py
index b925dd4..e693556 100644 (file)
@@ -14,7 +14,9 @@
 
 import sys
 import argparse
+from configparser import ConfigParser
 import logging
+from logging.handlers import RotatingFileHandler
 import os
 from threading import Thread
 import time
@@ -36,31 +38,32 @@ class LoggingSSLSocket(ssl.SSLSocket):
         try:
             result = super(LoggingSSLSocket, self).accept(*args, **kwargs)
         except Exception as ex:
-            logging.warn('SSLSocket.accept raised exception: %s', str(ex))
+            logging.warning('SSLSocket.accept raised exception: %s', str(ex))
             raise
         return result
 
 
 class InstallationWorker(Thread):
-    def __init__(self, server, uuid, admin_passwd, logdir, args=None):
+    def __init__(self, server, uuid, admin_passwd, yaml, logdir, args=None):
         super(InstallationWorker, self).__init__(name=uuid)
         self._server = server
         self._uuid = uuid
         self._admin_passwd = admin_passwd
+        self._yaml = yaml
         self._logdir = logdir
         self._args = args
 
     def run(self):
-        access_info = None
+        installer = Installer(self._server, self._uuid, self._yaml, self._logdir, self._args)
+        access_info = installer.get_access_info()
+
         if self._args:
             try:
-                installer = Installer(self._args)
-                #access_info = installer.install()
-
+                installer.install()
                 logging.info('Installation triggered for %s', self._uuid)
             except InstallException as ex:
-                logging.warn('Installation triggering failed for %s: %s', self._uuid, str(ex))
-                self._server.set_state(self._uuid, 'failed', str(ex), 0)
+                logging.warning('Installation triggering failed for %s: %s', self._uuid, str(ex))
+                self._server.set_state(self._uuid, 'failed', str(ex))
                 return
 
         installation_finished = False
@@ -69,15 +72,18 @@ class InstallationWorker(Thread):
             if not state['status'] == 'ongoing':
                 installation_finished = True
             else:
+                logging.info('Installation of %s still ongoing (%s%%): %s',
+                             self._uuid,
+                             state['percentage'],
+                             state['description'])
                 time.sleep(10)
 
         logging.info('Installation finished for %s: %s', self._uuid, state)
-        if access_info:
-            logging.info('Login details for installation %s: %s', self._uuid, str(access_info))
+        logging.info('Login details for installation %s: %s', self._uuid, str(access_info))
 
-        logging.info('Getting logs for installation %s...', uuid)
-        #installer.get_logs(self._logdir, self._admin_passwd)
-        logging.info('Logs retrieved for %s', uuid)
+        logging.info('Getting logs for installation %s...', self._uuid)
+        installer.get_logs(self._admin_passwd)
+        logging.info('Logs retrieved for %s', self._uuid)
 
 class Server(object):
     DEFAULT_PATH = '/opt/remoteinstaller'
@@ -85,10 +91,19 @@ class Server(object):
     ISO_PATH = 'images'
     CERTIFICATE_PATH = 'certificates'
     INSTALLATIONS_PATH = 'installations'
-    #CLOUD_ISO_PATH = '{}/rec.iso'.format(ISO_PATH)
-    BOOT_ISO_PATH = '{}/boot.iso'.format(ISO_PATH)
-
-    def __init__(self, host, port, cert=None, key=None, client_cert=None, client_key=None, ca_cert=None, path=None, http_port=None):
+    USER_CONFIG_NAME = 'user_config.yaml'
+    EXTRA_CONFIG_NAME = 'installation.ini'
+
+    def __init__(self,
+                 host,
+                 port,
+                 cert=None,
+                 key=None,
+                 client_cert=None,
+                 client_key=None,
+                 ca_cert=None,
+                 path=None,
+                 http_port=None):
         self._host = host
         self._port = port
         self._http_port = http_port
@@ -113,10 +128,20 @@ class Server(object):
         with open('{}/{}/{}/admin_passwd'.format(self._path,
                                                  Server.USER_CONFIG_PATH,
                                                  cloud_name)) as pwf:
-            admin_passwd = pwf.readline()
+            admin_passwd = pwf.readline().strip()
 
         return admin_passwd
 
+    def _get_yaml_path_for_cloud(self, cloud_name):
+        yaml = '{}/{}/{}/{}'.format(self._path,
+                                    Server.USER_CONFIG_PATH,
+                                    cloud_name,
+                                    Server.USER_CONFIG_NAME)
+        if not os.path.isfile(yaml):
+            raise ServerError('YAML file {} not found'.format(yaml))
+
+        return yaml
+
     def _load_states(self):
         uuid_list = os.listdir('{}/{}'.format(self._path, Server.INSTALLATIONS_PATH))
         for uuid in uuid_list:
@@ -130,14 +155,17 @@ class Server(object):
                     logdir = '{}/{}/{}'.format(self._path, Server.INSTALLATIONS_PATH, uuid)
                     cloud_name = self._ongoing_installations[uuid]['cloud_name']
                     admin_passwd = self._read_admin_passwd(cloud_name)
-                    worker = InstallationWorker(self, uuid, admin_passwd, logdir)
+                    yaml = self._get_yaml_path_for_cloud(cloud_name)
+                    worker = InstallationWorker(self, uuid, admin_passwd, yaml, logdir)
                     worker.start()
 
-    def _set_state(self, uuid, status, description, percentage, cloud_name=None):
-        self._ongoing_installations[uuid] = {}
+    def _set_state(self, uuid, status, description, percentage=None, cloud_name=None):
+        if not self._ongoing_installations.get(uuid, None):
+            self._ongoing_installations[uuid] = {}
         self._ongoing_installations[uuid]['status'] = status
         self._ongoing_installations[uuid]['description'] = description
-        self._ongoing_installations[uuid]['percentage'] = percentage
+        if percentage is not None:
+            self._ongoing_installations[uuid]['percentage'] = percentage
         if cloud_name:
             self._ongoing_installations[uuid]['cloud_name'] = cloud_name
 
@@ -145,9 +173,9 @@ class Server(object):
         with open(state_file, 'w') as sf:
             sf.write(json.dumps(self._ongoing_installations[uuid]))
 
-    def set_state(self, uuid, status, description, percentage):
-        logging.info('uuid=%s, status=%s, description=%s, percentage=%s',
-                     uuid, status, description, percentage)
+    def set_state(self, uuid, status, description, percentage=None):
+        logging.debug('set_state called for %s: status=%s, description=%s, percentage=%s',
+                      uuid, status, description, percentage)
 
         if not uuid in self._ongoing_installations:
             raise ServerError('Installation id {} not found'.format(uuid))
@@ -158,7 +186,7 @@ class Server(object):
         self._set_state(uuid, status, description, percentage)
 
     def get_state(self, uuid):
-        logging.info('uuid=%s', uuid)
+        logging.debug('get_state called for %s', uuid)
 
         if not uuid in self._ongoing_installations:
             raise ServerError('Installation id {} not found'.format(uuid))
@@ -167,23 +195,46 @@ class Server(object):
                 'description': self._ongoing_installations[uuid]['description'],
                 'percentage': self._ongoing_installations[uuid]['percentage']}
 
-    def start_installation(self, cloud_name, iso):
-        logging.info('start_installation(%s, %s)', cloud_name, iso)
+    def _read_extra_args(self, cloud_name):
+        extra = {}
+
+        extra_config_filename = '{}/{}/{}/{}'.format(self._path,
+                                    Server.USER_CONFIG_PATH,
+                                    cloud_name,
+                                    Server.EXTRA_CONFIG_NAME)
+
+        if os.path.isfile(extra_config_filename):
+            logging.debug('Read extra installation args from: %s', extra_config_filename)
+            extra_config = ConfigParser()
+            with open(extra_config_filename, 'r') as extra_config_file:
+                extra_config.readfp(extra_config_file)
 
-        uuid = str(uuid_module.uuid4())
+            if extra_config.has_section('extra'):
+                for key, value in extra_config.items('extra'):
+                    extra[key] = value
+
+        return extra
+
+    def start_installation(self, cloud_name, iso, boot_iso):
+        logging.debug('start_installation called with args: (%s, %s, %s)', cloud_name, iso, boot_iso)
+
+        uuid = str(uuid_module.uuid4())[:8]
 
         args = argparse.Namespace()
 
-        args.yaml = '{}/{}/{}/user_config.yml'.format(self._path,
-                                                      Server.USER_CONFIG_PATH,
-                                                      cloud_name)
-        if not os.path.isfile(args.yaml):
-            raise ServerError('YAML file {} not found'.format(args.yaml))
+        extra_args = self._read_extra_args(cloud_name)
+        vars(args).update(extra_args)
+
+        args.yaml = self._get_yaml_path_for_cloud(cloud_name)
 
         iso_path = '{}/{}/{}'.format(self._path, Server.ISO_PATH, iso)
         if not os.path.isfile(iso_path):
             raise ServerError('ISO file {} not found'.format(iso_path))
 
+        boot_iso_path = '{}/{}/{}'.format(self._path, Server.ISO_PATH, boot_iso)
+        if not os.path.isfile(boot_iso_path):
+            raise ServerError('Provisioning ISO file {} not found'.format(boot_iso_path))
+
         http_port_part = ''
         if self._http_port:
             http_port_part = ':{}'.format(self._http_port)
@@ -194,12 +245,12 @@ class Server(object):
 
         os.makedirs(args.logdir)
 
-        args.boot_iso = '{}/{}'.format(self._path, Server.BOOT_ISO_PATH)
+        args.boot_iso = '{}/{}/{}'.format(self._path, Server.ISO_PATH, boot_iso)
 
         args.tag = uuid
-        args.callback_url = 'http://{}:{}/v1/installations/{}/state'.format(self._host,
-                                                                            self._port,
-                                                                            uuid)
+        args.callback_url = 'https://{}:{}/v1/installations/{}/state'.format(self._host,
+                                                                             self._port,
+                                                                             uuid)
 
         args.client_cert = self._client_cert
         args.client_key = self._client_key
@@ -209,7 +260,7 @@ class Server(object):
         self._set_state(uuid, 'ongoing', '', 0, cloud_name)
 
         admin_passwd = self._read_admin_passwd(cloud_name)
-        worker = InstallationWorker(self, uuid, admin_passwd, args.logdir, args)
+        worker = InstallationWorker(self, uuid, admin_passwd, args.yaml, args.logdir, args)
         worker.start()
 
         return uuid
@@ -336,6 +387,7 @@ class WSGIHandler(object):
                 {
                     'cloud-name': <name of the cloud>,
                     'iso': <iso image name>,
+                    'provisioning-iso': <boot iso image name>
                 }
             Response: http status set correctly
                 {
@@ -351,14 +403,14 @@ class WSGIHandler(object):
                 request = json.loads(rpc.req_body)
                 cloud_name = request['cloud-name']
                 iso = request['iso']
+                boot_iso = request['provisioning-iso']
 
-                uuid = self.server.start_installation(cloud_name, iso)
+                uuid = self.server.start_installation(cloud_name, iso, boot_iso)
 
                 rpc.rep_status = HTTPErrors.get_ok_status()
                 reply = {'uuid': uuid}
                 rpc.rep_body = json.dumps(reply)
         except KeyError as ex:
-            rpc.rep_status = HTTPErrors.get_request_not_ok_status()
             raise ServerError('Missing request parameter: {}'.format(str(ex)))
         except Exception as exp:  # pylint: disable=broad-except
             rpc.rep_status = HTTPErrors.get_internal_error_status()
@@ -380,17 +432,13 @@ class WSGIHandler(object):
 
         logging.debug('_get_state called')
         try:
-            if not rpc.req_body:
-                rpc.rep_status = HTTPErrors.get_request_not_ok_status()
-            else:
-                uuid = rpc.req_params['uuid']
+            uuid = rpc.req_params['uuid']
 
-                reply = self.server.get_state(uuid)
+            reply = self.server.get_state(uuid)
 
-                rpc.rep_status = HTTPErrors.get_ok_status()
-                rpc.rep_body = json.dumps(reply)
+            rpc.rep_status = HTTPErrors.get_ok_status()
+            rpc.rep_body = json.dumps(reply)
         except KeyError as ex:
-            rpc.rep_status = HTTPErrors.get_request_not_ok_status()
             raise ServerError('Missing request parameter: {}'.format(str(ex)))
         except Exception as exp:  # pylint: disable=broad-except
             rpc.rep_status = HTTPErrors.get_internal_error_status()
@@ -410,7 +458,7 @@ class WSGIHandler(object):
                 }
         """
 
-        logging.debug('set_state called')
+        logging.debug('_set_state called')
         try:
             if not rpc.req_body:
                 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
@@ -419,15 +467,13 @@ class WSGIHandler(object):
                 uuid = rpc.req_params['uuid']
                 status = request['status']
                 description = request['description']
-                percentage = request['percentage']
+                percentage = request.get('percentage', None)
 
                 self.server.set_state(uuid, status, description, percentage)
 
                 rpc.rep_status = HTTPErrors.get_ok_status()
                 reply = {}
                 rpc.rep_body = json.dumps(reply)
-        except ServerError:
-            raise
         except KeyError as ex:
             rpc.rep_status = HTTPErrors.get_request_not_ok_status()
             raise ServerError('Missing request parameter: {}'.format(str(ex)))
@@ -445,7 +491,11 @@ class WSGIHandler(object):
             rpc.req_filter = {}
         rpc.req_content_type = environ['CONTENT_TYPE']
         try:
-            rpc.req_content_size = int(environ['CONTENT_LENGTH'])
+            content_len = environ['CONTENT_LENGTH']
+            if not content_len:
+                rpc.req_content_size = 0
+            else:
+                rpc.req_content_size = int(content_len)
         except KeyError:
             rpc.req_content_size = 0
 
@@ -497,12 +547,12 @@ class WSGIHandler(object):
 
             self._read_body(rpc, environ)
 
-            logging.info('Calling %s with rpc=%s', action, str(rpc))
+            logging.debug('Calling %s with rpc=%s', action, str(rpc))
             actionfunc = getattr(self, action)
             actionfunc(rpc)
         except ServerError as ex:
             rpc.rep_status = HTTPErrors.get_request_not_ok_status()
-            rpc.rep_status += ','
+            rpc.rep_status += ', '
             rpc.rep_status += str(ex)
         except AttributeError:
             rpc.rep_status = HTTPErrors.get_internal_error_status()
@@ -510,10 +560,10 @@ class WSGIHandler(object):
             rpc.rep_status += 'Missing action function'
         except Exception as exp:  # pylint: disable=broad-except
             rpc.rep_status = HTTPErrors.get_internal_error_status()
-            rpc.rep_status += ','
+            rpc.rep_status += ', '
             rpc.rep_status += str(exp)
 
-        logging.info('Replying with rpc=%s', str(rpc))
+        logging.debug('Replying with rpc=%s', str(rpc))
         response_headers = [('Content-type', 'application/json')]
         start_response(rpc.rep_status, response_headers)
         return [rpc.rep_body]
@@ -534,19 +584,23 @@ def wrap_socket(sock, keyfile=None, certfile=None,
 
 def main():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-H', '--host', required=True, help='binding ip of the server')
-    parser.add_argument('-P', '--listen', required=True, help='binding port of the server')
-    parser.add_argument('-S', '--server', required=False, help='externally visible ip of the server')
-    parser.add_argument('-B', '--port', required=False, help='externally visible port of the server')
-    parser.add_argument('-C', '--cert', required=False, help='server cert file name')
-    parser.add_argument('-K', '--key', required=False, help='server private key file name')
-    parser.add_argument('-c', '--client-cert', required=False, help='client cert file name')
-    parser.add_argument('-k', '--client-key', required=False, help='client key file name')
-    parser.add_argument('-A', '--ca-cert', required=False, help='CA cert file name')
-    parser.add_argument('-p', '--path', required=False, help='path for remote installer files')
-    parser.add_argument('-T', '--http-port', required=False, help='port for HTTPD')
-    parser.add_argument('-d', '--debug', required=False, help='Debug level for logging',
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-H', '--host', required=True, metavar='<bind ip>', help='binding ip of the server')
+    parser.add_argument('-P', '--listen', required=True, metavar='<bind port>', help='binding port of the server')
+    parser.add_argument('-S', '--server', required=False, metavar='<external ip>', help='externally visible ip of the server')
+    parser.add_argument('-B', '--port', required=False, metavar='<external port>', help='externally visible port of the server')
+    parser.add_argument('-C', '--cert', required=False, metavar='<server cert file>', help='path to server cert file')
+    parser.add_argument('-K', '--key', required=False, metavar='<server key file>', help='path to server private key file')
+    parser.add_argument('-c', '--client-cert', required=False, metavar='<client cert file>', help='path to client cert file')
+    parser.add_argument('-k', '--client-key', required=False, metavar='<client key file>', help='path to client key file')
+    parser.add_argument('-A', '--ca-cert', required=False, metavar='<CA cert file>', help='path to CA cert file')
+    parser.add_argument('-p', '--path', required=False, metavar='<path to installer files>', help='path to remote installer files')
+    parser.add_argument('-T', '--http-port', required=False, metavar='<HTTPD port>', help='port for HTTPD')
+    parser.add_argument('-d', '--debug', required=False, help='set debug level for logging',
                         action='store_true')
+    parser.add_argument('--log-file', required=False, default='/var/log/remote-installer.log', metavar='<server log file>', help='path to server log file')
+    parser.add_argument('--log-file-max-size', type=int, default=5, required=False, metavar='<max size>', help='server log file max size in MB')
+    parser.add_argument('--log-file-max-count', type=int, default=10, required=False, metavar='<max count>', help='server log file count')
 
     args = parser.parse_args()
 
@@ -555,10 +609,18 @@ def main():
     else:
         log_level = logging.INFO
 
-    format = '%(asctime)s %(threadName)s:%(levelname)s %(message)s'
-    logging.basicConfig(stream=sys.stdout, level=log_level, format=format)
+    logformat = '%(asctime)s %(threadName)s:%(levelname)s %(message)s'
+    logging.basicConfig(stream=sys.stdout, level=log_level, format=logformat)
+
+    log_file_handler = RotatingFileHandler(args.log_file,
+                                           maxBytes=(args.log_file_max_size*1024*1024),
+                                           backupCount=args.log_file_max_count)
+    log_file_handler.setFormatter(logging.Formatter(logformat))
+    log_file_handler.setLevel(log_level)
+    logging.getLogger().addHandler(log_file_handler)
 
-    logging.debug('args: %s', args)
+    logging.info('remote-installer started')
+    logging.debug('remote-installer args: %s', args)
 
     host = args.server
     if not host: