def store_workout_info(workout_id, unit_id, user_mail, workout_duration, workout_type, networks, servers, routes, firewall_rules, assessment, student_instructions_url, student_entry): ts = str(calendar.timegm(time.gmtime())) new_workout = datastore.Entity( ds_client.key('cybergym-workout', workout_id)) new_workout.update({ 'unit_id': unit_id, 'user_email': user_mail, 'student_instructions_url': student_instructions_url, 'expiration': workout_duration, 'type': workout_type, 'start_time': ts, 'run_hours': 0, 'timestamp': ts, 'resources_deleted': False, 'running': False, 'misfit': False, 'networks': networks, 'servers': servers, 'routes': routes, 'firewall_rules': firewall_rules, 'assessment': assessment, 'complete': False, 'student_entry': student_entry }) ds_client.put(new_workout)
def unit_signup(unit_id): unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) if request.method == 'POST': workout_query = ds_client.query(kind='cybergym-workout') workout_query.add_filter('unit_id', '=', unit_id) claimed_workout = None for workout in list(workout_query.fetch()): if 'student_name' in workout: if workout['student_name'] == None or workout[ 'student_name'] == "": with ds_client.transaction(): claimed_workout = workout claimed_workout['student_name'] = request.form[ 'student_name'] ds_client.put(claimed_workout) if unit['build_type'] == 'arena': return redirect('/student/arena_landing/%s' % claimed_workout.key.name) else: return redirect('/student/landing/%s' % claimed_workout.key.name) else: with ds_client.transaction(): claimed_workout = workout claimed_workout['student_name'] = request.form[ 'student_name'] ds_client.put(claimed_workout) if unit['build_type'] == 'arena': return redirect('/student/arena_landing/%s' % claimed_workout.key.name) else: return redirect('/student/landing/%s' % claimed_workout.key.name) return render_template('unit_signup.html', unit_full=True) return render_template('unit_signup.html')
def create_dynamic_answer(self, workout, question_number, answer): """ Create a dynamic answer. Typically called when the workout starts up and creates an arbitrary flag or answer for the assessment. TODO: This functions has not been used or tested. @param workout: DataStore Entity holding the workout. @type workout: Dict @param question_number: The number to change @type question_number: Int @param answer: The answer to the assessment question @type answer: String @return: Status @rtype: Boolean """ workout_question = workout['assessment']['questions'][question_number] is_dynamic = workout_question.get('dynamic', False) if is_dynamic: workout_question = workout['assessment']['questions'][ question_number] has_changed = workout_question.get('has_changed', False) if not has_changed: workout_question['answer'] = answer workout_question['has_changed'] = True ds_client.put(workout) return True return False
def get_user_groups(self, user): """ Get the groups this users is authorized under for this Arena @param user: Email address of authenticated user @type user: str @return: List of groups assigned to the user @rtype: list """ user_groups = [] for group in self.UserGroups.ALL_GROUPS: if user in self.admin_info[group]: user_groups.append(group) if not user_groups and user not in self.admin_info[ self.UserGroups.PENDING]: cloud_log( LogIDs.USER_AUTHORIZATION, f"Unauthorized User: {user}. Adding to pending authorization", LOG_LEVELS.ERROR) self.admin_info[self.UserGroups.PENDING].append(user) ds_client.put(self.admin_info) cloud_log(LogIDs.USER_AUTHORIZATION, f"{user} logged in under groups {user_groups}", LOG_LEVELS.INFO) return user_groups
def change_workout_state(): if request.method == 'POST': request_data = request.get_json(force=True) response = {} response['workout_id'] = request_data['workout_id'] response['new_state'] = request_data['new_state'] workout = ds_client.get( ds_client.key('cybergym-workout', request_data['workout_id'])) if 'state' in workout: previous_state = workout['state'] workout['state'] = request_data['new_state'] g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "Workout {} state override: {} to {}".format( request_data['workout_id'], previous_state, request_data['new_state']), "previous_state": str(previous_state), "new_state": str(request_data['new_state']), "workout": str(request_data['workout_id']) }, severity=LOG_LEVELS.INFO) ds_client.put(workout) return json.dumps(response)
def register_workout_update(project, dnszone, workout_id, old_ip, new_ip): service = googleapiclient.discovery.build('dns', 'v1') key = ds_client.key('cybergym-workout', workout_id) workout = ds_client.get(key) change_body = { "deletions": [{ "kind": "dns#resourceRecordSet", "name": workout_id + dns_suffix + ".", "rrdatas": [old_ip], "type": "A", "ttl": 30 }], "additions": [{ "kind": "dns#resourceRecordSet", "name": workout_id + dns_suffix + ".", "rrdatas": [new_ip], "type": "A", "ttl": 30 }] } request = service.changes().create(project=project, managedZone=dnszone, body=change_body) response = request.execute() workout["external_ip"] = new_ip ds_client.put(workout)
def complete_verification(): if (request.method == 'POST'): workout_request = request.get_json(force=True) workout_id = workout_request['workout_id'] token = workout_request['token'] workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) token_exists = next(item for item in workout['assessment']['questions'] if item['key'] == token) token_pos = next( (i for i, item in enumerate(workout['assessment']['questions']) if item['key'] == token), None) if token_exists: logger.info( "Completion token matches. Setting the workout question %d to complete." % token_pos) workout['assessment']['questions'][token_pos]['complete'] = True ds_client.put(workout) logger.info('%s workout question %d marked complete.' % (workout_id, token_pos + 1)) return 'OK', 200 else: logger.info( "In complete_verification: Completion key %s does NOT exist in assessment dict! Aborting" % token)
def commit_to_cloud(self): """ Commits the parsed workout specification for multiple student builds to the cloud datastore. This first stores the unit to the datastore and then all the individual builds @return: None """ cloud_log(LogIDs.MAIN_APP, f"Creating unit {self.unit_id}", LOG_LEVELS.INFO) new_unit = datastore.Entity( ds_client.key('cybergym-unit', self.unit_id)) new_unit.update(self.new_unit) workout_ids = [] for cloud_ready_spec in self.cloud_ready_specs: workout_id = ''.join( random.choice(string.ascii_lowercase) for i in range(10)) workout_ids.append(workout_id) new_workout = datastore.Entity( ds_client.key('cybergym-workout', workout_id)) new_workout.update(cloud_ready_spec) ds_client.put(new_workout) # Store the server specifications for compute workouts if self.build_type == BuildTypes.COMPUTE: self._commit_workout_servers(workout_id, new_workout) new_unit['workouts'] = workout_ids ds_client.put(new_unit) # If this is an arena, then store all server configurations at this time. if self.build_type == BuildTypes.ARENA: CompetitionServerSpecToCloud( unit=new_unit, workout_ids=workout_ids, workout_specs=self.cloud_ready_specs).commit_to_cloud() return {'unit_id': self.unit_id, 'build_type': self.build_type}
def store_comment(comment_email, comment_subject, comment_text): new_comment = datastore.Entity(ds_client.key('cybergym-comments')) new_comment['comment_email'] = comment_email new_comment['subject'] = comment_subject new_comment['comment_text'] = comment_text new_comment['message_viewed'] = False new_comment['date'] = datetime.datetime.now() ds_client.put(new_comment)
def change_workout_expiration(): if request.method == "POST": request_data = request.get_json(force=True) if 'workout_id' in request_data: workout = ds_client.get( ds_client.key('cybergym-workout', request_data['workout_id'])) workout['expiration'] = request_data['new_expiration'] ds_client.put(workout) return json.dumps(str("Test"))
def store_student_feedback(feedback, workout_id): student_feedback = datastore.Entity( ds_client.key('cybergym-student-feedback', workout_id)) for key in feedback.keys(): student_feedback[key] = feedback[key] ds_client.put(student_feedback) print(student_feedback) return student_feedback
def store_class_info(teacher_email, num_students, class_name, student_auth): new_class = datastore.Entity(ds_client.key('cybergym-class')) new_class['teacher_email'] = teacher_email class_roster = [] for i in range(int(num_students)): class_roster.append("Student {}".format(i + 1)) new_class['roster'] = class_roster new_class['class_name'] = class_name new_class['student_auth'] = student_auth ds_client.put(new_class)
def stop_workout(workout_id): result = compute.instances().list(project=project, zone=zone, filter='name = {}*'.format(workout_id)).execute() workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) workout['state'] == "READY" ds_client.put(workout) if 'items' in result: for vm_instance in result['items']: response = compute.instances().stop(project=project, zone=zone, instance=vm_instance["name"]).execute() print("Workouts stopped") else: print("No workouts to stop")
def nuke_workout(workout_id): #Get workout information workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) unit = ds_client.get(ds_client.key('cybergym-unit', workout['unit_id'])) #Create new workout of same type workout_type = workout['type'] unit_name = unit['unit_name'] unit_id = workout['unit_id'] submitted_answers = None if 'submitted_answers' in workout: submitted_answers = workout['submitted_answers'] if 'expiration' in unit: expiration = unit['expiration'] else: expiration = 0 instructor_id = workout['user_email'] yaml_string = parse_workout_yaml(workout_type) unit_id, build_type, new_id = process_workout_yaml(yaml_string, workout_type, unit_name, 1, expiration, instructor_id, unit_id) if submitted_answers: new_workout = ds_client.get(ds_client.key('cybergym-workout', new_id)) new_workout['submitted_answers'] = submitted_answers unit_id = workout['unit_id'] #Get new workout information response = { "unit_id": unit_id, "build_type": build_type, "workout_id": new_id } if build_type == 'compute': pub_build_single_workout( workout_id=new_id, topic_name=workout_globals.ps_build_workout_topic) if workout_id in unit['workouts']: unit['workouts'].remove(workout_id) unit['workouts'].append(new_id) ds_client.put(unit) workout['misfit'] = True ds_client.put(workout) return json.dumps(response)
def _cloud_update_class(self): class_unit = { "unit_id": self.unit_id, "unit_name": self.unit_name, "workout_name": self.workout_name, "build_type": self.build_type, "timestamp": str(calendar.timegm(time.gmtime())) } if 'unit_list' not in self.class_record: unit_list = [] unit_list.append(class_unit) self.class_record['unit_list'] = unit_list else: self.class_record['unit_list'].append(class_unit) ds_client.put(self.class_record)
def check_user_level(): if (request.method == 'POST'): user_info = request.get_json(force=True) admin_info = ds_client.get( ds_client.key('cybergym-admin-info', 'cybergym')) response = {'authorized': False, 'admin': False} if user_info['user_email']: if user_info['user_email'] in admin_info['authorized_users']: response['authorized'] = True if user_info['user_email'] in admin_info['admins']: response['admin'] = True else: if user_info['user_email'] not in admin_info['pending_users']: admin_info['pending_users'].append(user_info['user_email']) ds_client.put(admin_info) return json.dumps(response)
def build_arena(self): """ @return: Status @rtype: bool """ self._get_build_allocation() if not self.allocation: # If there are not enough resource, delete the unit and workouts from the datastore before returning. ds_client.delete(self.unit_id) for workout in self.workouts: ds_client.delete(workout.key) servers = ds_client.query(kind='cybergym-server').add_filter( 'workout', '=', workout.key.name).fetch() for server in servers: ds_client.delete(server) servers = ds_client.query(kind='cybergym-server').add_filter( 'workout', '=', workout.key.name).fetch() for server in servers: ds_client.delete(server) return False _projects = list(self.allocation.keys()) i = 0 j = 0 publisher = topic_path = current_project = None for workout in self.workouts: if i == 0: current_project = _projects[j] j += 1 publisher = pubsub_v1.PublisherClient() topic_path = publisher.topic_path( current_project, workout_globals.ps_build_workout_topic) workout['build_project_location'] = current_project ds_client.put(workout) publisher.publish(topic_path, data=b'Cyber Gym Workout', workout_id=workout.key.name) i += 1 # If the index is greater than the current project's build allocation, reset the index, which triggers # a project change on the next iteration. if i >= self.allocation[current_project]: i = 0 return True
def start_arena(): if request.method == 'POST': unit_id = request.form['unit_id'] arena_unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) if 'time' not in request.form: arena_unit['arena']['run_hours'] = 2 else: arena_unit['arena']['run_hours'] = min( int(request.form['time']), workout_globals.MAX_RUN_HOURS) ds_client.put(arena_unit) start_time = time.gmtime(time.time()) time_string = str(start_time[3]) + ":" + str( start_time[4]) + ":" + str(start_time[5]) print(time_string) arena_unit['start_time'] = time_string ds_client.put(arena_unit) pub_start_vm(unit_id, 'start-arena') return redirect('/arena_list/%s' % (unit_id))
def start_all(): if (request.method == 'POST'): unit_id = request.form['unit_id'] workout_list = get_unit_workouts(unit_id) t_list = [] for workout_id in workout_list: workout = ds_client.get( ds_client.key('cybergym-workout', workout_id['name'])) if 'time' not in request.form: workout['run_hours'] = 2 else: workout['run_hours'] = min(int(request.form['time']), workout_globals.MAX_RUN_HOURS) ds_client.put(workout) pub_start_vm(workout_id['name']) return redirect("/workout_list/%s" % (unit_id))
def __init__(self): self.admin_info = ds_client.get( ds_client.key('cybergym-admin-info', 'cybergym')) if self.UserGroups.ADMINS not in self.admin_info: admin_email = myconfig.get_variable.config('admin_email') if not admin_email: cloud_log(LogIDs.MAIN_APP, f"Error: Admin Email is not set for this project!", LOG_LEVELS.ERROR) else: self.admin_info[self.UserGroups.ADMINS] = [admin_email] if self.UserGroups.AUTHORIZED not in self.admin_info: self.admin_info[self.UserGroups.AUTHORIZED] = [] if self.UserGroups.STUDENTS not in self.admin_info: self.admin_info[self.UserGroups.STUDENTS] = [] if self.UserGroups.PENDING not in self.admin_info: self.admin_info[self.UserGroups.PENDING] = [] ds_client.put(self.admin_info)
def store_unit_info(id, email, unit_name, workout_name, build_type, ts, workout_url_path, teacher_instructions_url, workout_description): new_unit = datastore.Entity(ds_client.key('cybergym-unit', id)) new_unit.update({ "unit_name": unit_name, "build_type": build_type, "workout_name": workout_name, "instructor_id": email, "timestamp": ts, 'workout_url_path': workout_url_path, "description": workout_description, "teacher_instructions_url": teacher_instructions_url, "workouts": [] }) ds_client.put(new_unit)
def _update_build_spec(self): """ Add credentials for the student entry to the workout, and add a default firewall rule to allow access to the student entry server. When the build type is an arena, each workout in the arena needs to have added the recently generated credentials. @param credentials: Credentials for users in a given build @type credentials: list of dict @param network_name: The name of the network the student entry server resides in @type: str @return: Status @rtype: bool """ # Build the firewall rule to allow external access to the student entry. firewall_rule = { 'name': 'allow-student-entry', 'network': self.student_entry_network, 'target_tags': ['student-entry'], 'protocol': None, 'ports': ['tcp/80,8080,443'], 'source_ranges': ['0.0.0.0/0'] } if self.type == BuildTypes.COMPUTE: if len(self.student_credentials) > 1: self.build['workout_credentials'] = self.student_credentials else: self.build['workout_user'] = self.student_credentials[0][ 'workout_user'] self.build['workout_password'] = self.student_credentials[0][ 'workout_password'] self.build['firewall_rules'].append(firewall_rule) elif self.type == BuildTypes.ARENA: for credential in self.student_credentials: workout_id = credential['workout_id'] student_user = credential['workout_user'] student_password = credential['workout_password'] workout = ds_client.get( ds_client.key('cybergym-workout', workout_id)) workout['workout_user'] = student_user workout['workout_password'] = student_password ds_client.put(workout) self.build['arena']['firewall_rules'].append(firewall_rule) ds_client.put(self.build) return True
def start_vm(): if (request.method == 'POST'): data = request.get_json(force=True) workout_id = None if 'workout_id' in data: workout_id = data['workout_id'] g_logger = log_client.logger(str(workout_id)) g_logger.log_text(str('Starting workout ' + workout_id)) workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) if 'time' not in request.form: workout['run_hours'] = 2 else: workout['run_hours'] = min(int(request.form['time']), workout_globals.MAX_RUN_HOURS) # workout['running'] = True ds_client.put(workout) pub_start_vm(workout_id) return 'VM Started'
def store_workout_container(unit_id, workout_id, workout_type, student_instructions_url, container_info, assessment): ts = str(calendar.timegm(time.gmtime())) new_workout = datastore.Entity( ds_client.key('cybergym-workout', workout_id)) new_workout.update({ 'unit_id': unit_id, 'build_type': 'container', 'type': workout_type, 'student_instructions_url': student_instructions_url, 'timestamp': ts, 'complete': False, 'container_info': container_info, 'assessment': assessment, 'state': 'RUNNING' }) ds_client.put(new_workout)
def update_workout_list(): if request.method == 'POST': if 'new_workout_name' and 'new_workout_display' in request.form: admin_info = ds_client.get( ds_client.key('cybergym-admin-info', 'cybergym')) workout_list = admin_info['workout_list'] if not any( workout['workout_name'] == request.form['new_workout_name'] for workout in workout_list): new_workout_info = { 'workout_name': request.form['new_workout_name'], 'workout_display_name': request.form['new_workout_display'] } workout_list.append(new_workout_info) ds_client.put(admin_info) return 'Complete' else: return 'Failed' else: return 'Failed'
def store_arena_workout(workout_id, unit_id, user_mail, timestamp, student_servers, student_instructions_url, assessment): new_workout = datastore.Entity( ds_client.key('cybergym-workout', workout_id)) new_workout.update({ 'unit_id': unit_id, 'type': 'arena', 'build_type': 'arena', 'student_instructions_url': student_instructions_url, 'start_time': timestamp, 'run_hours': 0, 'timestamp': timestamp, 'running': False, 'misfit': False, 'assessment': assessment, 'student_servers': student_servers, 'instructor_id': user_mail, 'teacher_email': user_mail }) ds_client.put(new_workout)
def add_arena_to_unit(unit_id, workout_duration, timestamp, networks, user_mail, servers, routes, firewall_rules, student_entry, student_entry_type, network_type, student_entry_username, student_entry_password): """ Adds the necessary build components to the unit to build a common network in which all students can interact. :param unit_id: The unit in which to add the arena build :param workout_duration: The number of days before deleting this arena :param start_time: Used for calculating how long an arena has run :param networks: The network to build for this arena :param servers: The servers to build for this arena :param routes: The routes to include for this arena :param firewall_rules: The firewall rules to add for the arena :param student_entry*: Parameters for building the guacamole connections through startup scripts :return: None """ unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) teams = [] teams.append(str(user_mail)) unit['arena'] = { 'expiration': workout_duration, 'start_time': timestamp, 'run_hours': 0, 'timestamp': timestamp, 'resources_deleted': False, 'running': False, 'misfit': False, 'networks': networks, 'servers': servers, 'routes': routes, 'firewall_rules': firewall_rules, 'student_entry': student_entry, 'student_entry_type': student_entry_type, 'student_entry_username': student_entry_username, 'student_entry_password': student_entry_password, 'student_network_type': network_type } unit['teams'] = teams ds_client.put(unit)
def reset_workout(workout_id): key = ds_client.key('cybergym-workout', workout_id) workout = ds_client.get(key) result = compute.instances().list( project=project, zone=zone, filter='name = {}*'.format(workout_id)).execute() try: if 'items' in result: for vm_instance in result['items']: response = compute.instances().reset( project=project, zone=zone, instance=vm_instance["name"]).execute() workout_globals.extended_wait(project, zone, response["id"]) started_vm = compute.instances().get( project=project, zone=zone, instance=vm_instance["name"]).execute() if 'accessConfigs' in started_vm['networkInterfaces'][0]: if 'natIP' in started_vm['networkInterfaces'][0][ 'accessConfigs'][0]: tags = started_vm['tags'] if tags: for item in tags['items']: if item == 'labentry': ip_address = started_vm[ 'networkInterfaces'][0][ 'accessConfigs'][0]['natIP'] register_workout_update( project, dnszone, workout_id, workout["external_ip"], ip_address) time.sleep(30) workout['running'] = True workout['start_time'] = str(calendar.timegm(time.gmtime())) ds_client.put(workout) print("Finished resetting %s" % workout_id) return True except (): print("Error in resetting VM for %s" % workout_id) return False
def admin_page(): admin_info = ds_client.get(ds_client.key('cybergym-admin-info', 'cybergym')) if request.method == "POST": response = {'action_completed': 'false'} data = request.get_json(force=True) if data['action'] == 'approve_user': if data['user_email'] in admin_info['pending_users']: admin_info['authorized_users'].append(data['user_email']) admin_info['pending_users'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' elif data['action'] == 'deny_user': if data['user_email'] in admin_info['pending_users']: admin_info['pending_users'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' elif data['action'] == 'promote_user': if data['user_email'] in admin_info['authorized_users']: admin_info['admins'].append(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' elif data['action'] == 'remove_user': if data['user_email'] in admin_info['authorized_users']: admin_info['authorized_users'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' elif data['action'] == 'revoke_access': if data['user_email'] in admin_info['admins']: admin_info['admins'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' return json.dumps(response) return render_template('admin_page.html', auth_config=auth_config, admin_info=admin_info)
def _upload_file(self, file, prompt, index): """ Handles storage of image files for workout assessment questions involving screenshots. Stores images in GCP Storage Bucket {project}_assessment_upload/{workout_id} @param file: Blob object for the image file @param prompt: Used to designate the question to which the image pertains @param index: The index of the question in the assessment object. Used to provide a unique name to the image upload in GCP """ from utilities.datastore_functions import store_student_upload upload_url = store_student_upload(self.workout_id, file[0], index) if upload_url: for prev_upload in self.uploaded_files: if prev_upload['question'] == prompt: prev_upload['storage_url'] = upload_url self.workout['uploaded_files'] = self.uploaded_files ds_client.put(self.workout) return upload_url user_input = {"question": prompt, "storage_url": upload_url} self.uploaded_files.append(user_input) self.workout['uploaded_files'] = self.uploaded_files ds_client.put(self.workout) return json.dumps(user_input)