def arena_landing(workout_id): workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) unit = ds_client.get(ds_client.key('cybergym-unit', workout['unit_id'])) student_instructions_url = None if 'student_instructions_url' in workout: student_instructions_url = workout['student_instructions_url'] assessment = guac_user = guac_pass = flags_found = None if 'workout_user' in workout: guac_user = workout['workout_user'] if 'workout_password' in workout: guac_pass = workout['workout_password'] if 'assessment' in workout: try: assessment, assessment_type = get_assessment_questions(workout) except TypeError: print("type errors") if (request.method == "POST"): return process_assessment(workout, workout_id, request, assessment) return render_template('arena_landing.html', description=unit['description'], assessment=assessment, running=workout['running'], unit_id=workout['unit_id'], dns_suffix=dns_suffix, guac_user=guac_user, guac_pass=guac_pass, arena_id=workout_id, student_instructions=student_instructions_url)
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 pub_manage_server(server_name, action, topic_name="manage-server"): server = ds_client.get(ds_client.key('cybergym-server', server_name)) workout_id = server.get('workout', None) workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) build_project_location = workout.get('build_project_location', project) publisher = pubsub_v1.PublisherClient() topic_path = publisher.topic_path(build_project_location, topic_name) publisher.publish(topic_path, data=b'Server Build', action=action.encode('utf-8'), server_name=server_name)
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 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 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 arena_list(unit_id): unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) build_type = unit['build_type'] workout_url_path = unit['workout_url_path'] workout_list = get_unit_workouts(unit_id) teacher_instructions_url = None if 'teacher_instructions_url' in unit: teacher_instructions_url = unit['teacher_instructions_url'] student_instructions_url = None if 'student_instructions_url' in unit: student_instructions_url = unit['student_instructions_url'] start_time = None if 'start_time' in unit: start_time = unit['start_time'] # start_time = time.gmtime(start_time) unit_teams = None if 'teams' in unit: unit_teams = unit['teams'] if (request.method == "POST"): return json.dumps(unit) if unit: return render_template('arena_list.html', unit_teams=unit_teams, teacher_instructions=teacher_instructions_url, workout_list=workout_list, student_instructions=student_instructions_url, description=unit['description'], unit_id=unit_id, start_time=start_time)
def workout_list(unit_id): unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) build_type = unit['build_type'] workout_url_path = unit['workout_url_path'] workout_list = get_unit_workouts(unit_id) teacher_instructions_url = None if 'teacher_instructions_url' in unit: teacher_instructions_url = unit['teacher_instructions_url'] #For updating individual workout ready state if (request.method == "POST"): if build_type == 'arena': return json.dumps(unit) return json.dumps(workout_list) if unit and len(str(workout_list)) > 0: return render_template('workout_list.html', build_type=build_type, workout_url_path=workout_url_path, workout_list=workout_list, unit_id=unit_id, description=unit['description'], teacher_instructions=teacher_instructions_url) else: return render_template('no_workout.html')
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 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 get_arena_ip_addresses_for_workout(workout_id): """ Returns the server to local IP address mapping for a given arena and student workout. Not all servers have a direct guacamole connection, and the IP addresses are displayed to the student for jumping to other hosts from their landing server. :param workout_id: The Workout ID associated with the given arena """ workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) unit_id = workout['unit_id'] arena_servers = [] for server_spec in workout['student_servers']: server = ds_client.get( ds_client.key('cybergym-server', f"{workout_id}-{server_spec['name']}")) if server: server_to_ip = {'name': server_spec['name']} nics = [] for interface in server['config']['networkInterfaces']: nic = { 'network': path.basename(interface['subnetwork']), 'ip': interface['networkIP'] } nics.append(nic) server_to_ip['nics'] = nics arena_servers.append(server_to_ip) # Also include any additional shared servers in the arena unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) if 'arena' in unit and 'servers' in unit['arena'] and unit['arena'][ 'servers']: for server_spec in unit['arena']['servers']: server = ds_client.get( ds_client.key('cybergym-server', f"{unit_id}-{server_spec['name']}")) if server: server_to_ip = {'name': server['name']} nics = [] for interface in server['config']['networkInterfaces']: nic = { 'network': path.basename(interface['subnetwork']), 'ip': interface['networkIP'] } nics.append(nic) server_to_ip['nics'] = nics arena_servers.append(server_to_ip) return arena_servers
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 pub_stop_vm(workout_id, topic_name='stop-vm'): publisher = pubsub_v1.PublisherClient() workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) build_project_location = workout.get('build_project_location', project) topic_path = publisher.topic_path(build_project_location, topic_name) data = 'Cyber Gym VM Stop Request' publisher.publish(topic_path, data=data.encode('utf-8'), workout_id=workout_id)
def __init__(self, workout_id): self.workout_id = workout_id self.workout = ds_client.get( ds_client.key('cybergym-workout', self.workout_id)) self.assessment_info = self.workout[ 'assessment'] if 'assessment' in self.workout else None self.submitted_answers = self.workout[ 'submitted_answers'] if 'submitted_answers' in self.workout else None self.uploaded_files = self.workout[ 'uploaded_files'] if 'uploaded_files' in self.workout else [] """ Get workout build type to determine how to process assessment submissions - Arena submissions add points to all workouts assigned to the same team - Regular workouts simply store the submitted answer / files in the datastore """ unit = ds_client.get( ds_client.key('cybergym-unit', self.workout['unit_id'])) self.workout_build_type = unit['build_type']
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 __init__(self, unit_id): self.allocation = {} self.unit_id = unit_id self.unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) unit_workouts = ds_client.query(kind='cybergym-workout') unit_workouts.add_filter("unit_id", "=", unit_id) self.workouts = list(unit_workouts.fetch()) self.workout_count = len(self.workouts) self.remaining_count = self.workout_count
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 admin_workout(workout_id): workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) if workout: workout_server_query = ds_client.query(kind='cybergym-server') workout_server_query.add_filter('workout', '=', workout_id) server_list = [] for server in list(workout_server_query.fetch()): server_list.append(server) return render_template('admin_workout.html', workout=workout, servers=server_list) else: abort(404)
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 pub_nuke_workout(workout_id): workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) build_project_location = workout.get('build_project_location', project) publisher = pubsub_v1.PublisherClient() topic_path = publisher.topic_path(build_project_location, PUBSUB_TOPICS.BUILD_WORKOUTS) data = "Workout nuke and rebuild request" publisher.publish(topic_path, data=data.encode("utf-8"), workout_id=workout_id, action=WORKOUT_ACTIONS.NUKE)
def arena_landing(workout_id): workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) unit = ds_client.get(ds_client.key('cybergym-unit', workout['unit_id'])) arena_server_info = get_arena_ip_addresses_for_workout(workout_id) assessment_manager = CyberArenaAssessment(workout_id) student_instructions_url = None if 'student_instructions_url' in workout: student_instructions_url = workout['student_instructions_url'] formatted_instruction_url = student_instructions_url assessment = guac_user = guac_pass = flags_found = None if 'workout_user' in workout: guac_user = workout['workout_user'] if 'workout_password' in workout: guac_pass = workout['workout_password'] if 'assessment' in workout: try: assessment = assessment_manager.get_assessment_questions() except TypeError: g_logger = log_client.logger('cybergym-app-errors') g_logger.log_text( "TypeError encountered when submitting assessment for arena {}" .format(workout_id), severity=LOG_LEVELS.INFO) if (request.method == "POST"): attempt = assessment_manager.submit() return jsonify(attempt) return render_template('arena_landing.html', description=unit['description'], unit_id=unit.key.name, assessment=assessment, workout=workout, dns_suffix=dns_suffix, guac_user=guac_user, guac_pass=guac_pass, arena_id=workout_id, student_instructions=formatted_instruction_url, server_info=arena_server_info)
def _get_build_allocation(self): """ Identify the build allocation among the parent and all children based on available resources and identified quotas. @param workout_count: Number of workouts to build in this request @type workout_count: Integer @return: The parent/or children projects to use, which may split across multiple projects @rtype: Dict {'<parent>': 80, '<child1>': 300, '<child2>': 300} """ # First, get all available projects. available = {project: self.MAX_BUILDS} admin_info = ds_client.get( ds_client.key(AdminInfoEntity.KIND, 'cybergym')) if AdminInfoEntity.Entities.CHILD_PROJECTS in admin_info: for child_project in admin_info[ AdminInfoEntity.Entities.CHILD_PROJECTS]: available[child_project] = self.MAX_BUILDS # Next find all active workouts for the application query_workouts = ds_client.query(kind='cybergym-workout') query_workouts.add_filter('active', '=', True) # Determine the available build left for each parent and children projects. for workout in list(query_workouts.fetch()): workout_project = workout.get('build_project_location', project) if workout_project not in available: cloud_log( LogIDs.MAIN_APP, f"Error in workout specification. The project {workout_project} is not " f"a valid project for this application.", LOG_LEVELS.ERROR) return False available[workout_project] -= 1 # Set the build allocation for this project based on availability. for _project in available: workouts_available = available[_project] if workouts_available > 0: _project_allocation = min(self.remaining_count, workouts_available) self.allocation[_project] = _project_allocation self.remaining_count -= _project_allocation if self.remaining_count > 0: cloud_log( LogIDs.MAIN_APP, "Error: Not enough available resources to complete the build!", LOG_LEVELS.ERROR) return False else: return True
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 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 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 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 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 __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 _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