def update_rpi_sense_hat_data(self,
                                  device_num_id,
                                  ts,
                                  sensor_data,
                                  memory=None,
                                  ip=None,
                                  storage=None):
        """
        Performs a database upsert for the given device_num_id. It will delete the current row for device_num_id,
        and insert a row with the given data.
        @param device_num_id: Primary key for the database
        @type device_num_id: String
        @param ts: Timestamp from the IoT device
        @type ts: String
        @param sensor_data: json data from the IoT device
        @type sensor_data: String
        @param memory: The amount of memory in the IoT device
        @type memory: String
        @param ip: Current IP address of the IoT device
        @type ip: String
        @param storage: Remaining storage in the IoT device
        @type storage: String
        @return: None
        """
        dbcur = self.dbcon.cursor()
        iot_upsert = f"""
                        REPLACE INTO {self.RPI_SENSE_HAT}
                            (device_num_id, ts, sensor_data, memory, ip, storage)
                        VALUES 
                            (%s, %s, %s, %s, %s, %s)
                        """
        iot_args = (device_num_id, ts, sensor_data, memory, ip, storage)

        try:
            return_value = dbcur.execute(iot_upsert, iot_args)
        except pymysql.err.ProgrammingError as ex:
            # If the table does not exist, then create it first.
            if self._create_rpi_sense_hat_database():
                dbcur.execute(iot_upsert, iot_args)
            else:
                cloud_log(LogIDs.IOT,
                          "Unknown error: Could not create table for IoT.",
                          LOG_LEVELS.ERROR)
        finally:
            self.dbcon.commit()
            self.dbcon.close()
        return return_value
    def build(self):
        key = ds_client.key('cybergym-unit', self.unit_id)
        unit = ds_client.get(key)
        # This can sometimes happen when debugging a Unit ID and the Datastore record no longer exists.
        arena = unit['arena']
        if not arena:
            cloud_log(
                self.unit_id,
                f"Build operation failed. No unit {self.unit_id} exists in the data store",
                LOG_LEVELS.ERROR)
            raise LookupError

        if 'state' not in unit or not unit['state']:
            state_transition(entity=unit, new_state=BUILD_STATES.START)

        # STATE: BUILDING_STUDENT_NETWORKS
        if check_ordered_arenas_state(
                unit, BUILD_STATES.BUILDING_ARENA_STUDENT_NETWORKS):
            cloud_log(self.unit_id,
                      f"Creating student networks for arena {self.unit_id}",
                      LOG_LEVELS.INFO)
            student_network = [{
                'name':
                self.STUDENT_NETWORK_NAME,
                'subnets': [{
                    'name':
                    f'{self.unit_id}-{self.STUDENT_NETWORK_NAME}-default',
                    'ip_subnet': self.STUDENT_NETWORK_SUBNET
                }]
            }]
            try:
                create_network(networks=student_network, build_id=self.unit_id)
            except HttpError as err:
                if err.resp.status not in [409]:
                    cloud_log(
                        self.unit_id,
                        f"Error when trying to create the student network for "
                        f"the arena {self.unit_id}", LOG_LEVELS.ERROR)
                    raise

        # STATE: BUILDING_ARENA_NETWORKS
        if check_ordered_arenas_state(unit,
                                      BUILD_STATES.BUILDING_ARENA_NETWORKS):
            state_transition(entity=unit,
                             new_state=BUILD_STATES.BUILDING_ARENA_NETWORKS)
            if arena['networks']:
                cloud_log(
                    self.unit_id,
                    f"Creating additional arena networks for arena {self.unit_id}",
                    LOG_LEVELS.INFO)
                create_network(networks=arena['networks'],
                               build_id=self.unit_id)
            state_transition(entity=unit,
                             new_state=BUILD_STATES.COMPLETED_ARENA_NETWORKS)

        # STATE: BUILDING_ARENA_SERVERS
        if check_ordered_arenas_state(unit,
                                      BUILD_STATES.BUILDING_ARENA_SERVERS):
            state_transition(entity=unit,
                             new_state=BUILD_STATES.BUILDING_ARENA_SERVERS)
            cloud_log(self.unit_id,
                      f"Creating additional servers for arena {self.unit_id}",
                      LOG_LEVELS.INFO)
            server_list = list(
                ds_client.query(kind='cybergym-server').add_filter(
                    'workout', '=', self.unit_id).fetch())
            for server in server_list:
                server_name = server['name']
                cloud_log(self.unit_id,
                          f"Sending pubsub message to build {server_name}",
                          LOG_LEVELS.INFO)
                pubsub_topic = PUBSUB_TOPICS.MANAGE_SERVER
                publisher = pubsub_v1.PublisherClient()
                topic_path = publisher.topic_path(project, pubsub_topic)
                publisher.publish(topic_path,
                                  data=b'Server Build',
                                  server_name=server_name,
                                  action=SERVER_ACTIONS.BUILD)
            state_transition(entity=unit,
                             new_state=BUILD_STATES.COMPLETED_ARENA_SERVERS)

        # STATE: BUILDING_ROUTES
        if check_ordered_arenas_state(unit, BUILD_STATES.BUILDING_ROUTES):
            state_transition(entity=unit,
                             new_state=BUILD_STATES.BUILDING_ROUTES)
            cloud_log(
                self.unit_id,
                f"Creating network routes and firewall rules for arena {self.unit_id}",
                LOG_LEVELS.INFO)
            if 'routes' in arena and arena['routes']:
                for route in arena['routes']:
                    r = {
                        "name":
                        f"{self.unit_id}-{route['name']}",
                        "network":
                        f"{self.unit_id}-{route['network']}",
                        "destRange":
                        route['dest_range'],
                        "nextHopInstance":
                        f"{self.unit_id}-{route['next_hop_instance']}"
                    }
                    create_route(route)
            state_transition(entity=unit,
                             new_state=BUILD_STATES.COMPLETED_ROUTES)

        # STATE: BUILDING_FIREWALL
        if check_ordered_arenas_state(unit, BUILD_STATES.BUILDING_FIREWALL):
            state_transition(entity=unit,
                             new_state=BUILD_STATES.BUILDING_FIREWALL)
            firewall_rules = []
            for rule in arena['firewall_rules']:
                if 'network' not in rule:
                    rule['network'] = self.STUDENT_NETWORK_NAME
                firewall_rules.append({
                    "name": f"{self.unit_id}-{rule['name']}",
                    "network": f"{self.unit_id}-{rule['network']}",
                    "targetTags": rule['target_tags'],
                    "protocol": rule['protocol'],
                    "ports": rule['ports'],
                    "sourceRanges": rule['source_ranges']
                })

            # Create the default rules to allow traffic between student networks.
            firewall_rules.append({
                "name": f"{self.unit_id}-allow-all-internal",
                "network": f"{self.unit_id}-{self.STUDENT_NETWORK_NAME}",
                "targetTags": [],
                "protocol": 'tcp',
                "ports": ['tcp/any', 'udp/any', 'icmp/any'],
                "sourceRanges": [self.STUDENT_NETWORK_SUBNET]
            })

            create_firewall_rules(firewall_rules)
            state_transition(entity=unit,
                             new_state=BUILD_STATES.COMPLETED_FIREWALL)