3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
19 from threading import Thread
24 import uuid as uuid_module
27 from wsgiref.simple_server import make_server
30 from remoteinstaller.installer.install import Installer
31 from remoteinstaller.installer.install import InstallException
34 class LoggingSSLSocket(ssl.SSLSocket):
35 def accept(self, *args, **kwargs):
37 result = super(LoggingSSLSocket, self).accept(*args, **kwargs)
38 except Exception as ex:
39 logging.warn('SSLSocket.accept raised exception: %s', str(ex))
44 class InstallationWorker(Thread):
45 def __init__(self, server, uuid, admin_passwd, logdir, args=None):
46 super(InstallationWorker, self).__init__(name=uuid)
49 self._admin_passwd = admin_passwd
57 installer = Installer(self._args)
58 #access_info = installer.install()
60 logging.info('Installation triggered for %s', self._uuid)
61 except InstallException as ex:
62 logging.warn('Installation triggering failed for %s: %s', self._uuid, str(ex))
63 self._server.set_state(self._uuid, 'failed', str(ex), 0)
66 installation_finished = False
67 while not installation_finished:
68 state = self._server.get_state(self._uuid)
69 if not state['status'] == 'ongoing':
70 installation_finished = True
74 logging.info('Installation finished for %s: %s', self._uuid, state)
76 logging.info('Login details for installation %s: %s', self._uuid, str(access_info))
78 logging.info('Getting logs for installation %s...', uuid)
79 #installer.get_logs(self._logdir, self._admin_passwd)
80 logging.info('Logs retrieved for %s', uuid)
83 DEFAULT_PATH = '/opt/remoteinstaller'
84 USER_CONFIG_PATH = 'user-configs'
86 CERTIFICATE_PATH = 'certificates'
87 INSTALLATIONS_PATH = 'installations'
88 #CLOUD_ISO_PATH = '{}/rec.iso'.format(ISO_PATH)
89 BOOT_ISO_PATH = '{}/boot.iso'.format(ISO_PATH)
91 def __init__(self, host, port, cert=None, key=None, client_cert=None, client_key=None, ca_cert=None, path=None, http_port=None):
94 self._http_port = http_port
98 self._path = Server.DEFAULT_PATH
100 self._cert = '{}/{}/{}'.format(self._path, Server.CERTIFICATE_PATH, cert)
101 self._key = '{}/{}/{}'.format(self._path, Server.CERTIFICATE_PATH, key)
102 self._client_cert = '{}/{}/{}'.format(self._path, Server.CERTIFICATE_PATH, client_cert)
103 self._client_key = '{}/{}/{}'.format(self._path, Server.CERTIFICATE_PATH, client_key)
104 self._ca_cert = '{}/{}/{}'.format(self._path, Server.CERTIFICATE_PATH, ca_cert)
106 self._ongoing_installations = {}
109 def get_server_keys(self):
110 return {'cert': self._cert, 'key': self._key, 'ca_cert': self._ca_cert}
112 def _read_admin_passwd(self, cloud_name):
113 with open('{}/{}/{}/admin_passwd'.format(self._path,
114 Server.USER_CONFIG_PATH,
116 admin_passwd = pwf.readline()
120 def _load_states(self):
121 uuid_list = os.listdir('{}/{}'.format(self._path, Server.INSTALLATIONS_PATH))
122 for uuid in uuid_list:
123 state_file_name = '{}/{}/{}.state'.format(self._path, Server.INSTALLATIONS_PATH, uuid)
124 if os.path.exists(state_file_name):
125 with open(state_file_name) as sf:
126 state_json = sf.readline()
127 self._ongoing_installations[uuid] = json.loads(state_json)
129 if self._ongoing_installations[uuid]['status'] == 'ongoing':
130 logdir = '{}/{}/{}'.format(self._path, Server.INSTALLATIONS_PATH, uuid)
131 cloud_name = self._ongoing_installations[uuid]['cloud_name']
132 admin_passwd = self._read_admin_passwd(cloud_name)
133 worker = InstallationWorker(self, uuid, admin_passwd, logdir)
136 def _set_state(self, uuid, status, description, percentage, cloud_name=None):
137 self._ongoing_installations[uuid] = {}
138 self._ongoing_installations[uuid]['status'] = status
139 self._ongoing_installations[uuid]['description'] = description
140 self._ongoing_installations[uuid]['percentage'] = percentage
142 self._ongoing_installations[uuid]['cloud_name'] = cloud_name
144 state_file = '{}/{}/{}.state'.format(self._path, Server.INSTALLATIONS_PATH, uuid)
145 with open(state_file, 'w') as sf:
146 sf.write(json.dumps(self._ongoing_installations[uuid]))
148 def set_state(self, uuid, status, description, percentage):
149 logging.info('uuid=%s, status=%s, description=%s, percentage=%s',
150 uuid, status, description, percentage)
152 if not uuid in self._ongoing_installations:
153 raise ServerError('Installation id {} not found'.format(uuid))
155 if not status in ['ongoing', 'failed', 'completed']:
156 raise ServerError('Invalid state: {}'.format(status))
158 self._set_state(uuid, status, description, percentage)
160 def get_state(self, uuid):
161 logging.info('uuid=%s', uuid)
163 if not uuid in self._ongoing_installations:
164 raise ServerError('Installation id {} not found'.format(uuid))
166 return {'status': self._ongoing_installations[uuid]['status'],
167 'description': self._ongoing_installations[uuid]['description'],
168 'percentage': self._ongoing_installations[uuid]['percentage']}
170 def start_installation(self, cloud_name, iso):
171 logging.info('start_installation(%s, %s)', cloud_name, iso)
173 uuid = str(uuid_module.uuid4())
175 args = argparse.Namespace()
177 args.yaml = '{}/{}/{}/user_config.yml'.format(self._path,
178 Server.USER_CONFIG_PATH,
180 if not os.path.isfile(args.yaml):
181 raise ServerError('YAML file {} not found'.format(args.yaml))
183 iso_path = '{}/{}/{}'.format(self._path, Server.ISO_PATH, iso)
184 if not os.path.isfile(iso_path):
185 raise ServerError('ISO file {} not found'.format(iso_path))
189 http_port_part = ':{}'.format(self._http_port)
191 args.iso = 'https://{}{}/{}/{}'.format(self._host, http_port_part, Server.ISO_PATH, iso)
193 args.logdir = '{}/{}/{}'.format(self._path, Server.INSTALLATIONS_PATH, uuid)
195 os.makedirs(args.logdir)
197 args.boot_iso = '{}/{}'.format(self._path, Server.BOOT_ISO_PATH)
200 args.callback_url = 'http://{}:{}/v1/installations/{}/state'.format(self._host,
204 args.client_cert = self._client_cert
205 args.client_key = self._client_key
206 args.ca_cert = self._ca_cert
207 args.host_ip = self._host
209 self._set_state(uuid, 'ongoing', '', 0, cloud_name)
211 admin_passwd = self._read_admin_passwd(cloud_name)
212 worker = InstallationWorker(self, uuid, admin_passwd, args.logdir, args)
218 class ServerError(Exception):
222 class HTTPErrors(object):
223 # response for a successful GET, PUT, PATCH, DELETE,
224 # can also be used for POST that does not result in creation.
226 # response to a POST which results in creation.
228 # response to a successfull request that won't be returning any body like a DELETE request
229 HTTP_NO_CONTENT = 204
230 # used when http caching headers are in play
231 HTTP_NOT_MODIFIED = 304
232 # the request is malformed such as if the body does not parse
233 HTTP_BAD_REQUEST = 400
234 # when no or invalid authentication details are provided.
235 # also useful to trigger an auth popup API is used from a browser
236 HTTP_UNAUTHORIZED_OPERATION = 401
237 # when authentication succeeded but authenticated user doesn't have access to the resource
239 # when a non-existent resource is requested
241 # when an http method is being requested that isn't allowed for the authenticated user
242 HTTP_METHOD_NOT_ALLOWED = 405
243 # indicates the resource at this point is no longer available
245 # if incorrect content type was provided as part of the request
246 HTTP_UNSUPPORTED_MEDIA_TYPE = 415
247 # used for validation errors
248 HTTP_UNPROCESSABLE_ENTITY = 422
249 # when request is rejected due to rate limiting
250 HTTP_TOO_MANY_REQUESTS = 429
252 HTTP_INTERNAL_ERROR = 500
256 return '%d OK' % HTTPErrors.HTTP_OK
259 def get_object_created_successfully_status():
260 return '%d Created' % HTTPErrors.HTTP_CREATED
263 def get_request_not_ok_status():
264 return '%d Bad request' % HTTPErrors.HTTP_BAD_REQUEST
267 def get_resource_not_found_status():
268 return '%d Not found' % HTTPErrors.HTTP_NOT_FOUND
271 def get_unsupported_content_type_status():
272 return '%d Unsupported content type' % HTTPErrors.HTTP_UNSUPPORTED_MEDIA_TYPE
275 def get_validation_error_status():
276 return '%d Validation error' % HTTPErrors.HTTP_UNPROCESSABLE_ENTITY
279 def get_internal_error_status():
280 return '%d Internal error' % HTTPErrors.HTTP_INTERNAL_ERROR
283 class HTTPRPC(object):
289 self.req_content_type = ''
290 self.req_content_size = 0
297 return str.format('REQ: body:{body} filter:{filter} '
298 'params:{params} method:{method} path:{path} '
299 'content_type:{content_type} content_size:{content_size} '
300 'REP: body:{rep_body} status:{status}',
301 body=self.req_body, filter=self.req_filter,
302 params=str(self.req_params), method=self.req_method, path=self.req_path,
303 content_type=self.req_content_type, content_size=self.req_content_size,
304 rep_body=self.rep_body, status=self.rep_status)
306 class WSGIHandler(object):
307 def __init__(self, server):
308 logging.debug('WSGIHandler constructor called')
312 self.mapper = routes.Mapper()
313 self.mapper.connect(None, '/apis', action='get_apis')
314 self.mapper.connect(None, '/{api}/installations', action='handle_installations')
315 self.mapper.connect(None, '/{api}/installations/{uuid}/state', action='handle_state')
317 def handle_installations(self, rpc):
318 if rpc.req_method == 'POST':
319 self._start_installation(rpc)
321 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
322 rpc.rep_status += ', only POST are possible to this resource'
324 def handle_state(self, rpc):
325 if rpc.req_method == 'GET':
327 elif rpc.req_method == 'POST':
330 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
331 rpc.rep_status += ', only GET/POST are possible to this resource'
333 def _start_installation(self, rpc):
335 Request: POST http://<ip:port>/v1/installations
337 'cloud-name': <name of the cloud>,
338 'iso': <iso image name>,
340 Response: http status set correctly
342 'uuid': <operation identifier>
346 logging.debug('_start_installation called')
349 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
351 request = json.loads(rpc.req_body)
352 cloud_name = request['cloud-name']
355 uuid = self.server.start_installation(cloud_name, iso)
357 rpc.rep_status = HTTPErrors.get_ok_status()
358 reply = {'uuid': uuid}
359 rpc.rep_body = json.dumps(reply)
360 except KeyError as ex:
361 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
362 raise ServerError('Missing request parameter: {}'.format(str(ex)))
363 except Exception as exp: # pylint: disable=broad-except
364 rpc.rep_status = HTTPErrors.get_internal_error_status()
365 rpc.rep_status += ','
366 rpc.rep_status += str(exp)
368 def _get_state(self, rpc):
370 Request: GET http://<ip:port>/v1/installations/<uuid>/state
373 Response: http status set correctly
375 'status': <ongoing|completed|failed>,
376 'description': <description about the progress>,
377 'percentage': <percentage completed of the installation>
381 logging.debug('_get_state called')
384 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
386 uuid = rpc.req_params['uuid']
388 reply = self.server.get_state(uuid)
390 rpc.rep_status = HTTPErrors.get_ok_status()
391 rpc.rep_body = json.dumps(reply)
392 except KeyError as ex:
393 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
394 raise ServerError('Missing request parameter: {}'.format(str(ex)))
395 except Exception as exp: # pylint: disable=broad-except
396 rpc.rep_status = HTTPErrors.get_internal_error_status()
397 rpc.rep_status += ','
398 rpc.rep_status += str(exp)
400 def _set_state(self, rpc):
402 Request: POST http://<ip:port>/v1/installations/<uuid>/state
404 'status': <ongoing|completed|failed>,
405 'description': <description about the progress>,
406 'percentage': <percentage completed of the installation>
408 Response: http status set correctly
413 logging.debug('set_state called')
416 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
418 request = json.loads(rpc.req_body)
419 uuid = rpc.req_params['uuid']
420 status = request['status']
421 description = request['description']
422 percentage = request['percentage']
424 self.server.set_state(uuid, status, description, percentage)
426 rpc.rep_status = HTTPErrors.get_ok_status()
428 rpc.rep_body = json.dumps(reply)
431 except KeyError as ex:
432 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
433 raise ServerError('Missing request parameter: {}'.format(str(ex)))
434 except Exception as exp: # pylint: disable=broad-except
435 rpc.rep_status = HTTPErrors.get_internal_error_status()
436 rpc.rep_status += ','
437 rpc.rep_status += str(exp)
439 def _read_header(self, rpc, environ):
440 rpc.req_method = environ['REQUEST_METHOD']
441 rpc.req_path = environ['PATH_INFO']
443 rpc.req_filter = urlparse.parse_qs(urllib.unquote(environ['QUERY_STRING']))
446 rpc.req_content_type = environ['CONTENT_TYPE']
448 rpc.req_content_size = int(environ['CONTENT_LENGTH'])
450 rpc.req_content_size = 0
452 def _get_action(self, rpc):
453 # get the action to be done
455 match_result = self.mapper.match(rpc.req_path)
457 rpc.rep_status = HTTPErrors.get_resource_not_found_status()
458 raise ServerError('URL does not match')
461 if isinstance(match_result, dict):
462 resultdict = match_result
464 resultdict = match_result[0]
467 action = resultdict['action']
468 for key, value in resultdict.iteritems():
470 rpc.req_params[key] = value
472 rpc.rep_status = HTTPErrors.get_internal_error_status()
473 raise ServerError('No action found')
477 def _read_body(self, rpc, environ):
478 # get the body if available
479 if rpc.req_content_size:
480 if rpc.req_content_type == 'application/json':
481 rpc.req_body = environ['wsgi.input'].read(rpc.req_content_size)
483 rpc.rep_status = HTTPErrors.get_unsupported_content_type_status()
484 raise ServerError('Content type is not json')
486 def __call__(self, environ, start_response):
487 logging.debug('Handling request started, environ=%s', str(environ))
489 # For request and resonse data
491 rpc.rep_status = HTTPErrors.get_ok_status()
494 self._read_header(rpc, environ)
496 action = self._get_action(rpc)
498 self._read_body(rpc, environ)
500 logging.info('Calling %s with rpc=%s', action, str(rpc))
501 actionfunc = getattr(self, action)
503 except ServerError as ex:
504 rpc.rep_status = HTTPErrors.get_request_not_ok_status()
505 rpc.rep_status += ','
506 rpc.rep_status += str(ex)
507 except AttributeError:
508 rpc.rep_status = HTTPErrors.get_internal_error_status()
509 rpc.rep_status += ','
510 rpc.rep_status += 'Missing action function'
511 except Exception as exp: # pylint: disable=broad-except
512 rpc.rep_status = HTTPErrors.get_internal_error_status()
513 rpc.rep_status += ','
514 rpc.rep_status += str(exp)
516 logging.info('Replying with rpc=%s', str(rpc))
517 response_headers = [('Content-type', 'application/json')]
518 start_response(rpc.rep_status, response_headers)
519 return [rpc.rep_body]
521 def wrap_socket(sock, keyfile=None, certfile=None,
522 server_side=False, cert_reqs=ssl.CERT_NONE,
523 ssl_version=ssl.PROTOCOL_SSLv23, ca_certs=None,
524 do_handshake_on_connect=True,
525 suppress_ragged_eofs=True,
528 return LoggingSSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
529 server_side=server_side, cert_reqs=cert_reqs,
530 ssl_version=ssl_version, ca_certs=ca_certs,
531 do_handshake_on_connect=do_handshake_on_connect,
532 suppress_ragged_eofs=suppress_ragged_eofs,
536 parser = argparse.ArgumentParser()
537 parser.add_argument('-H', '--host', required=True, help='binding ip of the server')
538 parser.add_argument('-P', '--listen', required=True, help='binding port of the server')
539 parser.add_argument('-S', '--server', required=False, help='externally visible ip of the server')
540 parser.add_argument('-B', '--port', required=False, help='externally visible port of the server')
541 parser.add_argument('-C', '--cert', required=False, help='server cert file name')
542 parser.add_argument('-K', '--key', required=False, help='server private key file name')
543 parser.add_argument('-c', '--client-cert', required=False, help='client cert file name')
544 parser.add_argument('-k', '--client-key', required=False, help='client key file name')
545 parser.add_argument('-A', '--ca-cert', required=False, help='CA cert file name')
546 parser.add_argument('-p', '--path', required=False, help='path for remote installer files')
547 parser.add_argument('-T', '--http-port', required=False, help='port for HTTPD')
548 parser.add_argument('-d', '--debug', required=False, help='Debug level for logging',
551 args = parser.parse_args()
554 log_level = logging.DEBUG
556 log_level = logging.INFO
558 format = '%(asctime)s %(threadName)s:%(levelname)s %(message)s'
559 logging.basicConfig(stream=sys.stdout, level=log_level, format=format)
561 logging.debug('args: %s', args)
571 server = Server(host, port, args.cert, args.key, args.client_cert, args.client_key, args.ca_cert, args.path, args.http_port)
573 wsgihandler = WSGIHandler(server)
575 wsgi_server = make_server(args.host, int(args.listen), wsgihandler)
578 server_keys = server.get_server_keys()
579 wsgi_server.socket = wrap_socket(wsgi_server.socket,
580 certfile=server_keys['cert'],
581 keyfile=server_keys['key'],
583 ca_certs=server_keys['ca_cert'],
584 cert_reqs=ssl.CERT_REQUIRED)
586 wsgi_server.serve_forever()
588 if __name__ == "__main__":