def tearDown(self): # Create fresh grafana instance (in case test invalidated any tokens, etc.) self.grafana = Grafana(self.config) # Clear all of the created dashboards self.grafana.delete_dashboard_by_name(self.event_name) self.grafana.delete_datasource_by_name(self.datasource_name)
def setUp(self): # api keys with admin and viewer level permissions self.ADMIN = Grafana.create_api_key(HOST, "admin", "Admin") self.VIEWER = Grafana.create_api_key(HOST, "viewer", "Viewer") self.login_url = "mercury:EventAccess" self.sensor_url = "mercury:sensor" self.event_url = "mercury:events" self.config_url = "mercury:gfconfig" self.config_update_url = "mercury:gfconfig_update" self.config_delete_url = "mercury:gfconfig_delete" self.config_update_dashboard_url = "mercury:gfconfig_update_dashboard" self.config_reset_dashboard_url = "mercury:gfconfig_reset_dashboard" self.config_delete_dashboard_url = "mercury:gfconfig_delete_dashboard" self.config_add_dashboard_url = "mercury:gfconfig_create_dashboard" test_code = EventCodeAccess(event_code="testcode", enabled=True) test_code.save() # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) self.gfconfig = GFConfig.objects.create(gf_name="Test", gf_host=HOST, gf_token=self.ADMIN, gf_current=True) self.gfconfig.save() # Create fresh grafana object self.grafana = Grafana(self.gfconfig) # Create random name to be used for event and datasource self.event_name = self.grafana.generate_random_string(10) self.datasource_name = self.grafana.generate_random_string(10) # Clear existing dashboard and datasource self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources()
def delete_sensor(request, sensor_name): """This deletes a sensor from the database based on a button click""" sensor_to_delete = AGSensor.objects.get(name=sensor_name) sensor_type_to_delete = AGSensorType.objects.get(name=sensor_name) if sensor_type_to_delete: # delete any sensor panels from grafana gfconfigs = GFConfig.objects.all() events = AGEvent.objects.all() # delete panl from every dashboard of all grafana instances for gfconfig in gfconfigs: grafana = Grafana(gfconfig) # Delete sensor from each event panel for event in events: try: grafana.delete_panel(sensor_to_delete.name, event) except ValueError as error: messages.error(request, error) sensor_to_delete.delete() sensor_type_to_delete.delete() else: messages.error(request, sensor_name, " not found!!!") return redirect("/sensor")
def tearDown(self): # Create fresh grafana instance (in case test invalidated any tokens, etc.) self.grafana = Grafana(self.gfconfig) # Clear all of the created dashboards self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources()
def setUp(self): # api keys with admin and viewer level permissions self.ADMIN = Grafana.create_api_key(HOST, "admin", "Admin") self.VIEWER = Grafana.create_api_key(HOST, "viewer", "Viewer") self.login_url = "mercury:EventAccess" self.sensor_url = "mercury:sensor" self.event_url = "mercury:events" self.event_delete_url = "mercury:delete_event" self.event_update_url = "mercury:update_event" self.update_sensor_url = "mercury:update_sensor" self.delete_sensor_url = "mercury:delete_sensor" self.update_sensor_type_url = "mercury:update_type" test_code = EventCodeAccess(event_code="testcode", enabled=True) test_code.save() # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create fresh grafana object self.config = self.create_gfconfig() self.grafana = Grafana(self.config) # Create random name to be used for event and datasource self.event_name = self.grafana.generate_random_string(10) self.updated_event_name = self.event_name + " Day Two" self.datasource_name = self.grafana.generate_random_string(10) # Clear existing dashboard and datasource self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources()
def post(self, request, *args, **kwargs): if "submit" in request.POST: DB = settings.DATABASES config_data = GFConfig( gf_name=request.POST.get("gf_name"), gf_host=request.POST.get("gf_host"), gf_token=request.POST.get("gf_token"), gf_db_host=DB["default"]["HOST"] + ":" + str(DB["default"]["PORT"]), gf_db_name=DB["default"]["NAME"], gf_db_username=DB["default"]["USER"], gf_db_pw=DB["default"]["PASSWORD"], ) # Create Grafana instance with host and token grafana = Grafana(config_data) try: grafana.create_postgres_datasource() except ValueError as error: messages.error(request, f"Datasource couldn't be created. {error}") try: grafana.validate_credentials() config_data.gf_current = True # Deprecated # Only save the config if credentials were validated config_data.save() # If any events exist, add a dashboard for each event # If any sensors exist, add them to each event dashboard events = AGEvent.objects.all() sensors = AGSensor.objects.all() for event in events: grafana.create_dashboard(event.name) for sensor in sensors: grafana.add_panel(sensor, event) except ValueError as error: messages.error(request, f"Grafana initial set up failed: {error}") # Prepare an array of dicts with details for each GFConfig (GFConfig object, # list of dashboards/forms # Retrieve all available GFConfigs current_configs = GFConfig.objects.all().order_by("id") config_form = GFConfigForm() context = {"config_form": config_form, "configs": current_configs} return render(request, self.template_name, context)
def configure_dashboard(request, gf_id=None): config = GFConfig.objects.get(id=gf_id) # Prepare a dict with details for this GFConfig (GFConfig object, # list of dashboards/forms config_info = dict() config_info["config"] = config # Create Grafana class to handle this GF instance grafana = Grafana(config) try: grafana.validate_credentials() existing_events = grafana.get_all_events() current_dashboards = grafana.get_all_dashboards() except ValueError as error: messages.error(request, f"Cannot connect to Grafana: {error}") existing_events = [] current_dashboards = [] # Assemble a list of dicts w/: url, sensors, initialized sensor form, # and dashboard name # Retrieve missing events to pass to the context all_events = list(AGEvent.objects.all()) missing_events = list(set(all_events) - set(existing_events)) # Prepare an array of dashboards & their sensors to send to the template dashboards = [] for dashboard in current_dashboards: dashboard_dict = dict() # Get all currently used panels to initialize the form existing_sensors = grafana.get_all_sensors(dashboard["title"]) # Set initial form data so that only existing sensors are checked sensor_form = DashboardSensorPanelsForm( initial={"sensors": existing_sensors}) # Retrieve the URL for this dashboard or "" dashboard_url = grafana.get_dashboard_url_by_name(dashboard["title"]) if dashboard_url is None: dashboard_url = "" # Store everything in a list of dicts dashboard_dict["sensor_form"] = sensor_form dashboard_dict["dashboard_url"] = dashboard_url dashboard_dict["sensors"] = AGSensor.objects.all() dashboard_dict["name"] = dashboard["title"] dashboards.append(dashboard_dict) config_info["dashboards"] = dashboards config_info["missing_events"] = missing_events context = {"config": config_info} return render(request, "gf_dashboards.html", context)
def reset_dashboard(request, gf_id=None): gfconfig = GFConfig.objects.filter(id=gf_id).first() if gfconfig: grafana = Grafana(gfconfig) dashboard_name = request.POST.get("dashboard_name") sensors = AGSensor.objects.all() grafana.update_dashboard_panels(dashboard_name, sensors) msg = "Successfully reset dashboard: {}".format(dashboard_name) messages.success(request, msg) else: messages.error( request, "Unable to reset dashboard, Grafana instance not found") return redirect("/gfconfig/configure/{}".format(gf_id))
def delete_dashboard(request, gf_id=None): gfconfig = GFConfig.objects.filter(id=gf_id).first() if gfconfig: grafana = Grafana(gfconfig) dashboard_name = request.POST.get("dashboard_name") # try to delete the dashboard if grafana.delete_dashboard_by_name(dashboard_name) is False: messages.error(request, "Unable to delete dashboard") else: msg = "Successfully deleted dashboard: {}".format(dashboard_name) messages.success(request, msg) else: messages.error( request, "Unable to delete dashboard, Grafana instance not found") return redirect("/gfconfig/configure/{}".format(gf_id))
def update_dashboard(request, gf_id=None): gfconfig = GFConfig.objects.filter(id=gf_id).first() if gfconfig: grafana = Grafana(gfconfig) dashboard_name = request.POST.get("dashboard_name") sensors = request.POST.getlist("sensors") sensor_objects = [] for sensor in sensors: sensor = AGSensor.objects.filter(id=sensor).first() sensor_objects.append(sensor) grafana.update_dashboard_panels(dashboard_name, sensor_objects) msg = "Successfully updated dashboard: {}".format(dashboard_name) messages.success(request, msg) else: messages.error( request, "Unable to update dashboard, Grafana instance not found") return redirect("/gfconfig/configure/{}".format(gf_id))
def update_event(request, event_uuid=None): event_to_update = AGEvent.objects.get(uuid=event_uuid) if event_to_update: new_name = request.POST.get("name") new_venue_id = request.POST.get("venue_uuid") new_venue_object = AGVenue.objects.get(uuid=new_venue_id) new_description = request.POST.get("description") # if the name was changed, update all Grafana dashboards with new name if new_name != event_to_update.name: gfconfigs = GFConfig.objects.all() for gfconfig in gfconfigs: # Grafana instance using current GFConfig grafana = Grafana(gfconfig) try: dashboard = grafana.update_dashboard_title( event_to_update, new_name) if dashboard: messages.success( request, f"{gfconfig.gf_name}: Grafana " f"dashboard title updated", ) except ValueError as error: messages.error( request, f"{gfconfig.gf_name}: Grafana dashboard " f"title not updated: {error} ", ) # update the AGEvent object event_to_update.name = new_name event_to_update.venue_uuid = new_venue_object event_to_update.description = new_description event_to_update.save() messages.success(request, "Event updated successfully") return redirect("/events")
def delete_event(request, event_uuid=None): event_to_delete = AGEvent.objects.get(uuid=event_uuid) # delete any dashboards that exist for this event gfconfigs = GFConfig.objects.all() # Add panel to each grafana instance for gfconfig in gfconfigs: # Grafana instance using current GFConfig grafana = Grafana(gfconfig) deleted = grafana.delete_dashboard_by_name(event_to_delete.name) if not deleted: messages.error( request, f"Failed to delete Event dashboard from Grafana instance: " f"{gfconfig.gf_host}", ) event_to_delete.delete() return redirect("/events")
def delete_config(request, gf_id=None): config = GFConfig.objects.get(id=gf_id) # Create Grafana class to handle this GF instance grafana = Grafana(config) try: grafana.validate_credentials() grafana.delete_all_datasources() grafana.delete_all_dashboards() except ValueError as error: messages.error(request, f"Grafana instance not deleted: {error}") else: messages.success(request, f"Grafana instance deleted successfully") config.delete() return redirect("/gfconfig")
def create_dashboard(request, gf_id=None): gfconfig = GFConfig.objects.filter(id=gf_id).first() event_name = request.POST.get("selected_event_name") event = AGEvent.objects.filter(name=event_name).first() sensors = AGSensor.objects.all() if gfconfig and event: grafana = Grafana(gfconfig) try: grafana.create_dashboard(event.name) for sensor in sensors: grafana.add_panel(sensor, event) msg = "Successfully created dashboard: {}".format(event_name) messages.success(request, msg) except ValueError as error: messages.error(f"Unable to add dashboard to Grafana: {error}") return redirect("/gfconfig/configure/{}".format(gf_id))
def post(self, request, *args, **kwargs): if "submit-venue" in request.POST: post_venue_name = request.POST.get("name") post_venue_description = request.POST.get("description") post_venue_longitude = request.POST.get("longitude") post_venue_latitude = request.POST.get("latitude") venue_data = AGVenue( name=post_venue_name, description=post_venue_description, longitude=post_venue_longitude, latitude=post_venue_latitude, ) venue_data.save() if "submit-event" in request.POST: post_event_name = request.POST.get("name") post_event_location_id = request.POST.get("venue_uuid") venue_object = AGVenue.objects.get(uuid=post_event_location_id) post_event_date = request.POST.get("date") post_event_description = request.POST.get("description") event_data = AGEvent( name=post_event_name, venue_uuid=venue_object, date=post_event_date, description=post_event_description, ) event_data.save() gfconfig = GFConfig.objects.filter(gf_current=True) # only create an event dashboard if a current gfconfig exists if gfconfig.count() > 0: dashboard = None # create a dashboard with the same name as the event config = gfconfig[0] try: grafana = Grafana(config) dashboard = grafana.create_dashboard(post_event_name) except ValueError as error: # pass any failure message from the API to the UI messages.error( request, f"Grafana dashboard for this event was not " f"created: {error}", ) # if a dashboard was created successfully, add panels to it if dashboard: # create a panel for each sensor sensors = AGSensor.objects.all() for sensor in sensors: try: grafana.add_panel(sensor, event_data) except ValueError as error: # pass any error messages from the API to the UI messages.error(request, error) events = AGEvent.objects.all().order_by("uuid") venues = AGVenue.objects.all().order_by("uuid") event_form = EventForm() venue_form = VenueForm() context = { "event_form": event_form, "venue_form": venue_form, "events": events, "venues": venues, } return render(request, self.template_name, context)
class TestGFConfig(TestCase): TESTCODE = "testcode" test_sensor_name = "Wind Sensor" test_sensor_type = "Dual wind" test_sensor_format = { "left_gust": { "unit": "km/h", "format": "float" }, "right_gust": { "unit": "km/h", "format": "float" }, } test_sensor_graph_type = "graph" test_event_data = { "name": "Sunny Day Test Drive", "date": datetime.datetime(2020, 2, 2, 20, 21, 22), "description": "A very progressive test run at \ Sunnyside Daycare's Butterfly Room.", "location": "New York, NY", } test_venue_data = { "name": "Venue 1", "description": "foo", "latitude": 100, "longitude": 200, } def create_venue_and_event(self, event_name): venue = AGVenue.objects.create( name=self.test_venue_data["name"], description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() event = AGEvent.objects.create( name=event_name, date=self.test_event_data["date"], description=self.test_event_data["description"], venue_uuid=venue, ) event.save() return event def setUp(self): # api keys with admin and viewer level permissions self.ADMIN = Grafana.create_api_key(HOST, "admin", "Admin") self.VIEWER = Grafana.create_api_key(HOST, "viewer", "Viewer") self.login_url = "mercury:EventAccess" self.sensor_url = "mercury:sensor" self.event_url = "mercury:events" self.config_url = "mercury:gfconfig" self.config_update_url = "mercury:gfconfig_update" self.config_delete_url = "mercury:gfconfig_delete" self.config_update_dashboard_url = "mercury:gfconfig_update_dashboard" self.config_reset_dashboard_url = "mercury:gfconfig_reset_dashboard" self.config_delete_dashboard_url = "mercury:gfconfig_delete_dashboard" self.config_add_dashboard_url = "mercury:gfconfig_create_dashboard" test_code = EventCodeAccess(event_code="testcode", enabled=True) test_code.save() # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) self.gfconfig = GFConfig.objects.create(gf_name="Test", gf_host=HOST, gf_token=self.ADMIN, gf_current=True) self.gfconfig.save() # Create fresh grafana object self.grafana = Grafana(self.gfconfig) # Create random name to be used for event and datasource self.event_name = self.grafana.generate_random_string(10) self.datasource_name = self.grafana.generate_random_string(10) # Clear existing dashboard and datasource self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources() def tearDown(self): # Create fresh grafana instance (in case test invalidated any tokens, etc.) self.grafana = Grafana(self.gfconfig) # Clear all of the created dashboards self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources() def _get_with_event_code(self, url, event_code): self.client.get(reverse(self.login_url)) self.client.post(reverse(self.login_url), data={"eventcode": event_code}) response = self.client.get(reverse(url)) session = self.client.session return response, session def test_config_view_get_success(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) response = self.client.get(reverse(self.config_url)) self.assertEqual(200, response.status_code) def test_config_view_get_existing_dashboard_displayed(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) venue = AGVenue.objects.create( name=self.test_venue_data["name"], description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # Send a request to create an event (should trigger the creation of a # grafana dashboard of the same name) self.client.post( reverse(self.event_url), data={ "submit-event": "", "name": self.event_name, "date": self.test_event_data["date"], "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) response = self.client.get("/gfconfig/configure/{}".format( self.gfconfig.id)) self.assertContains(response, self.event_name) self.assertContains(response, sensor.name) def test_config_post_success(self): # Delete GFConfig used for the tests (will interfere otherwise) self.gfconfig.delete() # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) response = self.client.post( reverse(self.config_url), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": self.ADMIN, "gf_username": "******", "gf_password": "******", }, ) self.assertEqual(302, response.status_code) # Restore GFConfig instance used for the tests self.gfconfig = GFConfig.objects.create(gf_name="Test", gf_host=HOST, gf_token=self.ADMIN, gf_current=True) self.gfconfig.save() gfconfig = GFConfig.objects.filter(gf_name="Test Grafana Instance") self.assertTrue(gfconfig.count() > 0) self.assertTrue(gfconfig[0].gf_name == "Test Grafana Instance") self.assertTrue(gfconfig[0].gf_host == HOST) self.assertTrue(gfconfig[0].gf_token == self.ADMIN) def test_config_post_fail_invalid_api_key(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) response = self.client.post( reverse(self.config_url), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": "abcde", }, ) self.assertEqual(302, response.status_code) gfconfig = GFConfig.objects.filter(gf_name="Test Grafana Instance") self.assertTrue(gfconfig.count() == 0) def test_config_post_fail_insufficient_permissions(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) response = self.client.post( reverse(self.config_url), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": self.VIEWER, "gf_username": "******", "gf_password": "******", }, ) self.assertEqual(302, response.status_code) gfconfig = GFConfig.objects.filter(gf_name="Test Grafana Instance") self.assertTrue(gfconfig.count() == 0) def test_delete_config(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) GFConfig.objects.all().delete() gfconfig = GFConfig.objects.create(gf_name="Test Grafana Instance", gf_host=HOST, gf_token=self.ADMIN) gfconfig.save() gfconfig = GFConfig.objects.filter( gf_name="Test Grafana Instance").first() self.client.post( os.path.join( reverse(self.config_delete_url, kwargs={"gf_id": gfconfig.id})), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": self.ADMIN, "gf_username": "******", "gf_password": "******", }, ) gfconfig = GFConfig.objects.filter(gf_name="Test Grafana Instance") self.assertTrue(gfconfig.count() == 0) # test that GFConfig.gf_current can be set to True using the update view def test_update_config(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) GFConfig.objects.all().delete() gfconfig = GFConfig.objects.create( gf_name="Test Grafana Instance", gf_host=HOST, gf_token=self.ADMIN, gf_current=False, ) gfconfig.save() self.client.post( os.path.join( reverse(self.config_update_url, kwargs={"gf_id": gfconfig.id}))) gfconfig = GFConfig.objects.all().first() self.assertEquals(gfconfig.gf_current, True) def test_config_post_event_exists_dashboard_created(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) self.create_venue_and_event(self.event_name) # Delete GFConfig used for the test (will interfere otherwise) self.gfconfig.delete() response = self.client.post( reverse(self.config_url), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": self.ADMIN, "gf_username": "******", "gf_password": "******", }, ) self.assertEqual(302, response.status_code) # Restore GFConfig instance used for the tests self.gfconfig = GFConfig.objects.create(gf_name="Test", gf_host=HOST, gf_token=self.ADMIN, gf_current=True) self.gfconfig.save() # check that dashboard was created with same name as event dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) self.assertEquals(dashboard["dashboard"]["title"], self.event_name) def test_config_post_event_exists_dashboard_created_with_sensor(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() self.create_venue_and_event(self.event_name) # Delete GFConfig used for the test (will interfere otherwise) self.gfconfig.delete() response = self.client.post( reverse(self.config_url), data={ "submit": "", "gf_name": "Test Grafana Instance", "gf_host": HOST, "gf_token": self.ADMIN, "gf_username": "******", "gf_password": "******", }, ) self.assertEqual(302, response.status_code) # Restore GFConfig instance used for the tests self.gfconfig = GFConfig.objects.create(gf_name="Test", gf_host=HOST, gf_token=self.ADMIN, gf_current=True) self.gfconfig.save() # check that dashboard was created with expected panel dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) self.assertEquals(dashboard["dashboard"]["title"], self.event_name) # panels should have been created # querying like this because the returned dashboard object may have no panels # attribute, so trying to retrieve dashboard["panels"] could throw a key error panels = dashboard["dashboard"].get("panels", None) self.assertTrue(panels) self.assertTrue(len(panels) == 1) panel = panels[0] self.assertEquals(panel["title"], self.test_sensor_name) def test_update_dashboard_panels_remove_all_single_gfconfig(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create an event event = self.create_venue_and_event(self.event_name) # Create a dashboard self.grafana.create_dashboard(self.event_name) # Add a panel to the dashboard # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # Add a sensor panel self.grafana.add_panel(sensor, event) # Update dashboard with empty list of sensors self.client.post( reverse(self.config_update_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={ "dashboard_name": self.event_name, "sensors": [] }, ) # Query dashboard dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] # Confirm panels were deleted self.assertEquals(panels, []) def test_update_dashboard_panels_keep_all_panels_single_gfconfig(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) self.create_venue_and_event(self.event_name) # create a dashboard self.grafana.create_dashboard(self.event_name) # add a panel to the dashboard # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() sensors = AGSensor.objects.all() sensor_ids = [] for sensor in sensors: sensor_ids.append(sensor.uuid) self.client.post( reverse(self.config_update_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={ "dashboard_name": self.event_name, "sensors": sensor_ids }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] self.assertEquals(len(panels), 1) def test_update_dashboard_panels_keep_subset_of_panels_single_gfconfig( self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) self.create_venue_and_event(self.event_name) # create a dashboard self.grafana.create_dashboard(self.event_name) # add a panel to the dashboard # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() # Create 5 sensors for i in range(5): sensor = AGSensor.objects.create(name=self.test_sensor_name + "i", type_id=sensor_type) sensor.save() # Retrieve sensor ids for the first 2 sensors sensor_ids = [] sensors = AGSensor.objects.all() for i in range(2): sensor_ids.append(sensors[i].uuid) # Post to update the dashboard with 2 sensor panels self.client.post( reverse(self.config_update_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={ "dashboard_name": self.event_name, "sensors": sensor_ids }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] self.assertEquals(len(panels), 2) for i in range(2): self.assertEquals(panels[i]["title"], sensors[i].name) def test_reset_dashboard_panels_single_gfconfig(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # update dashboard with a subset of panels, then restore all panels by using # reset self.create_venue_and_event(self.event_name) # create a dashboard self.grafana.create_dashboard(self.event_name) # add a panel to the dashboard # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() # Create 5 sensors for i in range(5): sensor = AGSensor.objects.create(name=self.test_sensor_name + "i", type_id=sensor_type) sensor.save() # Retrieve sensor ids for the first 2 sensors sensor_ids_partial = [] sensors = AGSensor.objects.all() for i in range(2): sensor_ids_partial.append(sensors[i].uuid) # Post to update the dashboard with 2 sensor panels self.client.post( reverse(self.config_update_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={ "dashboard_name": self.event_name, "sensors": sensor_ids_partial }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] self.assertEquals(len(panels), 2) sensor_ids = [] sensors = AGSensor.objects.all() for sensor in sensors: sensor_ids.append(sensor.uuid) # Post to reset the dashboard with all sensor panels self.client.post( reverse(self.config_reset_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={"dashboard_name": self.event_name}, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] self.assertEquals(len(panels), 5) for sensor in sensors: self.assertEquals(panels[i]["title"], sensor.name) def test_delete_dashboard_single_gfconfig(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # update dashboard with a subset of panels, then restore all panels by using # reset self.create_venue_and_event(self.event_name) # create a dashboard self.grafana.create_dashboard(self.event_name) # Post to update the dashboard with 2 sensor panels self.client.post( reverse(self.config_delete_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={"dashboard_name": self.event_name}, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) # No dashboard should exist with this name self.assertFalse(dashboard) # User can post to a create_dashboard view in GFConfig in order to add an event # dashboard to Grafana def test_add_event_dashboard_through_gfconfig_view(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create an event self.create_venue_and_event(self.event_name) # User posts to add_dashboard view self.client.post( reverse(self.config_add_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={"selected_event_name": self.event_name}, ) # Confirm dashboard added to Grafana dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) def test_add_event_dashboard_through_gfconfig_sensor_panels_added(self): # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create an event self.create_venue_and_event(self.event_name) # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # User posts to add_dashboard view self.client.post( reverse(self.config_add_dashboard_url, kwargs={"gf_id": self.gfconfig.id}), data={"selected_event_name": self.event_name}, ) # Confirm dashboard added with panel for existing sensor dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve current panels try: panels = dashboard["dashboard"]["panels"] except KeyError: panels = [] self.assertEquals(len(panels), 1) self.assertEquals(panels[0]["title"], sensor.name)
def post(self, request, *args, **kwargs): if "submit" not in request.POST: return DB = settings.DATABASES gf_host = request.POST.get("gf_host") gf_name = request.POST.get("gf_name") gf_username = request.POST.get("gf_username") gf_password = request.POST.get("gf_password") gf_token = request.POST.get("gf_token") # check whether gf_host already in use existing_host = GFConfig.objects.filter(gf_host=gf_host) if "update-config" not in request.POST and existing_host: messages.error( request, "Hostname {} already in use".format(gf_host), ) return redirect("/gfconfig") # a test is calling this with known gf_token if not gf_token: auth_url = make_auth_url(gf_host, gf_username, gf_password) try: gf_token = Grafana.create_api_key(auth_url, "mercury-auto-admin", "Admin") except ValueError as error: messages.error( request, "Failed to create API token: {}".format(error), ) return redirect("/gfconfig") # the user is submitting an update form if "update-config" in request.POST: existing_host.update( gf_username=gf_username, gf_password=gf_password, gf_token=gf_token, ) messages.success(request, "Updated Grafana host: {}".format(gf_host)) return redirect("/gfconfig") # new gfconfig record config_data = GFConfig( gf_name=gf_name, gf_host=gf_host, gf_username=gf_username, gf_password=gf_password, gf_token=gf_token, gf_db_host=DB["default"]["HOST"] + ":" + str(DB["default"]["PORT"]), gf_db_name=DB["default"]["NAME"], gf_db_username=DB["default"]["USER"], gf_db_pw=DB["default"]["PASSWORD"], ) # Create Grafana instance with host and token grafana = Grafana(config_data) try: grafana.validate_credentials() grafana.create_postgres_datasource() except ValueError as error: messages.error(request, error) try: config_data.gf_current = True # Deprecated # Only save the config if credentials were validated config_data.save() # If any events exist, add a dashboard for each event # If any sensors exist, add them to each event dashboard events = AGEvent.objects.all() sensors = AGSensor.objects.all() for event in events: grafana.create_dashboard(event.name) for sensor in sensors: grafana.add_panel(sensor, event) except ValueError as error: messages.error(request, f"Grafana initial set up failed: {error}") messages.success(request, "Created Grafana Host: {}".format(gf_name)) return redirect("/gfconfig")
def post(self, request, *args, **kwargs): sensor_name = request.POST.get("sensor-name") # name = type name field_names = request.POST.getlist("field-names") field_types = request.POST.getlist("data-types") field_units = request.POST.getlist("units") sensor_name, field_names = remove_whitespace_caps(sensor_name, field_names) graph_type = request.POST.get("sensor-graph-type") if "edit_sensor" in request.POST: new_name = request.POST.get("sensor-name-updated") new_name, field_names = remove_whitespace_caps(new_name, field_names) new = True if new_name == sensor_name: new = False valid, request = validate_inputs(new_name, field_names, request, new) if graph_type == "map" and len(field_names) != 2: messages.error( request, "Map panels must have exactly 2 fields for " "latitude and longitude GPS coordinates. Update " "the sensor fields or change the Graph Type.", ) valid = False new_format = generate_sensor_format(field_names, field_types, field_units) if valid: sensor_to_update = AGSensor.objects.get(name=sensor_name) prev_name = sensor_to_update.name sensor_type_to_update = AGSensorType.objects.get(name=sensor_name) prev_format = sensor_type_to_update.format prev_graph_type = sensor_type_to_update.graph_type sensor_to_update.name = new_name sensor_type_to_update.name = new_name sensor_type_to_update.format = new_format sensor_type_to_update.graph_type = graph_type sensor_to_update.save() sensor_type_to_update.save() # update sensor panel in each grafana instance gfconfigs = GFConfig.objects.all() events = AGEvent.objects.all() name_changed = True if new_name != prev_name else False format_changed = True if new_format != prev_format else False graph_type_changed = True if graph_type != prev_graph_type else False if name_changed and not (format_changed or graph_type_changed): for gfconfig in gfconfigs: grafana = Grafana(gfconfig) for event in events: # Update sensor panel in each event dashboard # instance try: grafana.update_panel_title(event, prev_name, new_name) except ValueError as error: messages.error(request, error) messages.success( request, f"Grafana panels updated based on sensor changes" ) elif format_changed or graph_type_changed: if format_changed: # Delete any existing measurement data for the sensor AGMeasurement.objects.filter( sensor_id=sensor_to_update.id ).delete() for gfconfig in gfconfigs: grafana = Grafana(gfconfig) for event in events: # Update sensor panel in each event dashboard # instance try: grafana.update_panel_sensor( event, prev_name, sensor_to_update ) except ValueError as error: messages.error(request, error) messages.success( request, f"Grafana panels updated based on sensor changes" ) else: messages.error(request, f"No changes detected - no updates made") if "submit_new_sensor" in request.POST: valid, request = validate_inputs( sensor_name, field_names, request, new=True ) sensor_format = generate_sensor_format( field_names, field_types, field_units ) graph_type = request.POST.get("sensor-graph-type") if valid: """1) The structure of the models (database API) is confusing and we hide the confusing details from the user. 2) Note that we have to first create a sensor type, save it then create a sensor which takes type as an input 3) Sensor types and sensors have the same name. They are the same concept and should not be separated. Separating sensors from sensor types will likely cause bad things to happen... """ new_type = AGSensorType.objects.create( name=sensor_name, processing_formula=0, format=sensor_format, graph_type=graph_type, ) new_type.save() sensor_type = AGSensorType.objects.get(name=sensor_name) new_sensor = AGSensor.objects.create( name=sensor_name, type_id=sensor_type ) new_sensor.save() # Add Sensor panel to the Active Event dashboard in each Grafana # instances gfconfigs = GFConfig.objects.all() # Retrieve the events events = AGEvent.objects.all() for event in events: # Add panel to each grafana instance for gfconfig in gfconfigs: # Grafana instance using current GFConfig grafana = Grafana(gfconfig) # Add the Sensor Panel to each event try: grafana.add_panel(new_sensor, event) except ValueError as error: messages.error( request, f"Failed to add panel to active dashboard: {error}", ) else: messages.success( request, "Sensor panel added to Grafana for the active event", ) # gather sensors and sensor types (which should be the same) and render them types = AGSensorType.objects.all() sensors = AGSensor.objects.all() graph_types = AGSensorType.GRAPH_CHOICES context = { "sensor_types": types, "sensors": sensors, "graph_types": graph_types, } return render(request, self.template_name, context)
class TestGrafana(TestCase): TESTCODE = "testcode" title = "Bar" test_sensor_name = "Wind Sensor" test_sensor_name_update = "Another Name" test_sensor_type = "Dual wind" test_sensor_format = { "left_gust": { "unit": "km/h", "format": "float" }, "right_gust": { "unit": "km/h", "format": "float" }, } test_sensor_graph_type = "graph" field_name_1 = "test-field-1" field_name_2 = "test-field-2" data_type_1 = "test-data-type-1" data_type_2 = "test-data-type-2" unit_1 = "test-unit-1" unit_2 = "test-unit-2" test_sensor = { "name": test_sensor_name, "processing formula": 0, "field-names": [field_name_1, field_name_2], "data-types": [data_type_1, data_type_2], "units": [unit_1, unit_2], } test_sensor_format_update = { "fuel reading": { "unit": "m", "format": "float" }, } test_event_data = { "name": "Sunny Day Test Drive", "date": datetime.datetime(2020, 2, 2, 20, 21, 22), "description": "A very progressive test run at \ Sunnyside Daycare's Butterfly Room.", "location": "New York, NY", } test_event_data_update = { "name": "Another Day Test Drive", "date": datetime.datetime(2020, 3, 3, 20, 21, 22), "description": "A very modern test run at \ my backyard.", "location": "Buffalo, NY", } test_venue_data = { "name": "Venue 1", "description": "foo", "latitude": 100, "longitude": 200, } def create_gfconfig(self): config = GFConfig.objects.create( gf_host=HOST, gf_token=self.ADMIN, gf_db_host=DB_HOSTNAME, gf_db_name=DB_NAME, gf_db_username=DB_USERNAME, gf_db_pw=DB_PASSWORD, gf_current=True, ) config.save() return config # Returns event def create_venue_and_event(self, event_name): venue = AGVenue.objects.create( name=self.test_venue_data["name"], description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() event = AGEvent.objects.create( name=event_name, date=self.test_event_data["date"], description=self.test_event_data["description"], venue_uuid=venue, ) event.save() return event def setUp(self): # api keys with admin and viewer level permissions self.ADMIN = Grafana.create_api_key(HOST, "admin", "Admin") self.VIEWER = Grafana.create_api_key(HOST, "viewer", "Viewer") self.login_url = "mercury:EventAccess" self.sensor_url = "mercury:sensor" self.event_url = "mercury:events" self.event_delete_url = "mercury:delete_event" self.event_update_url = "mercury:update_event" self.update_sensor_url = "mercury:update_sensor" self.delete_sensor_url = "mercury:delete_sensor" self.update_sensor_type_url = "mercury:update_type" test_code = EventCodeAccess(event_code="testcode", enabled=True) test_code.save() # Login self._get_with_event_code(self.sensor_url, self.TESTCODE) # Create fresh grafana object self.config = self.create_gfconfig() self.grafana = Grafana(self.config) # Create random name to be used for event and datasource self.event_name = self.grafana.generate_random_string(10) self.updated_event_name = self.event_name + " Day Two" self.datasource_name = self.grafana.generate_random_string(10) # Clear existing dashboard and datasource self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources() def tearDown(self): # Create fresh grafana instance (in case test invalidated any tokens, etc.) self.grafana = Grafana(self.config) # Clear all of the created dashboards self.grafana.delete_all_dashboards() self.grafana.delete_all_datasources() def _get_with_event_code(self, url, event_code): self.client.get(reverse(self.login_url)) self.client.post(reverse(self.login_url), data={"eventcode": event_code}) response = self.client.get(reverse(url)) session = self.client.session return response, session def test_get_dashboard_with_uid_success(self): # Should return True if create_dashboard was successful dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Query new dashboard uid = dashboard["uid"] fetched_dashboard = self.grafana.get_dashboard_with_uid(uid) # Confirm params of new dashboard (slug, uid, title) self.assertTrue(fetched_dashboard) self.assertTrue(fetched_dashboard["meta"]) self.assertTrue(fetched_dashboard["meta"]["slug"], self.event_name.lower()) self.assertTrue(fetched_dashboard["dashboard"]) self.assertTrue(fetched_dashboard["dashboard"]["uid"], uid) self.assertTrue(fetched_dashboard["dashboard"]["title"], self.event_name) def test_get_dashboard_url_success(self): self.grafana.create_dashboard(self.event_name) self.assertTrue(self.grafana.get_dashboard_url_by_name( self.event_name)) def test_get_dashboard_url_none(self): self.assertIsNone( self.grafana.get_dashboard_url_by_name(self.event_name + "foo")) def test_get_dashboard_with_uid_fail(self): # Create a random UID to search for uid = self.grafana.generate_random_string(10) # Try and fetch random UID (should return None) fetched_dashboard = self.grafana.get_dashboard_with_uid(uid) self.assertFalse(fetched_dashboard) def test_create_grafana_dashboard_success(self): # Should return an object with dashboard credentials dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) def test_create_grafana_dashboard_verify_new_dashboard_contents(self): # Should return an object with dashboard credentials dashboard = self.grafana.create_dashboard(self.event_name) # Confirm that an object was returned self.assertTrue(dashboard) # Check for success status message self.assertEquals(dashboard["status"], "success") # Check that dashboard has correct name self.assertEquals(dashboard["slug"], self.event_name.lower()) # Retrieve UID for new dashboard, use to query new dashboard uid = dashboard["uid"] # Confirm new dashboard can be queried from the API endpoint = os.path.join(self.grafana.endpoints["dashboard_uid"], uid) headers = {"Content-Type": "application/json"} response = requests.get(url=endpoint, headers=headers, auth=("api_key", self.grafana.api_token)) # Confirm uid and title are as expected self.assertEquals(response.json()["dashboard"]["uid"], uid) self.assertEquals(response.json()["dashboard"]["title"], self.event_name) def test_create_grafana_dashboard_fail_authorization(self): self.grafana.api_token = "abcde" # invalid API token expected_message = "Invalid API key" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_dashboard(self.event_name) def test_create_grafana_dashboard_fail_permissions(self): self.grafana.api_token = self.VIEWER # API token with viewer permissions expected_message = "Access denied - check API permissions" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_dashboard(self.event_name) def test_create_grafana_dashboard_fail_empty_title(self): expected_message = "Dashboard title cannot be empty" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_dashboard("") def test_validate_credentials_success(self): # should return True if credentials are valid self.assertTrue(self.grafana.validate_credentials()) def test_update_dashboard_title_success(self): event = self.create_venue_and_event(self.event_name) self.grafana.create_dashboard(self.event_name) new_name = event.name + " foo" self.assertTrue(self.grafana.update_dashboard_title(event, new_name)) def test_update_dashboard_title_fail_same_name(self): event = self.create_venue_and_event(self.event_name) self.grafana.create_dashboard(self.event_name) self.assertFalse(self.grafana.update_dashboard_title( event, event.name)) def test_update_dashboard_title_fail_no_dashboard(self): event = self.create_venue_and_event(self.event_name) self.assertFalse(self.grafana.update_dashboard_title( event, event.name)) def test_validate_credentials_fail_authorization(self): self.grafana.api_token = "abcde" # invalid API token expected_message = "Grafana API validation failed: Invalid API key" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.validate_credentials() def test_validate_credentials_fail_permissions(self): self.grafana.api_token = self.VIEWER # API token with viewer permissions expected_message = ("Grafana API validation failed: Access denied - " "check API permissions") with self.assertRaisesMessage(ValueError, expected_message): self.grafana.validate_credentials() def test_create_grafana_dashboard_fail_duplicate_title(self): # Should return a JSON object with a message that the dashboard already exists dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) expected_message = "Dashboard with the same name already exists" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_dashboard(self.event_name) def test_delete_grafana_dashboard(self): # Create a dashboard dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Delete the dashboard, should return True if deleted deleted_dashboard = self.grafana.delete_dashboard(dashboard["uid"]) self.assertTrue(deleted_dashboard) # Confirm that dashboard was deleted by querying it endpoint = os.path.join(self.grafana.endpoints["dashboard_uid"], dashboard["uid"]) headers = {"Content-Type": "application/json"} response = requests.get(url=endpoint, headers=headers, auth=("api_key", self.grafana.api_token)) self.assertTrue(response.json()["message"]) self.assertEquals(response.json()["message"], "Dashboard not found") def test_delete_grafana_dashboard_fail(self): # Generate a random uid uid = self.grafana.generate_random_string(10) # Attempt to delete dashboard that doesn't exist, should return False deleted_dashboard = self.grafana.delete_dashboard(uid) self.assertFalse(deleted_dashboard) def test_add_panel(self): # Create a dashboard, confirm it was created and retrieve its UID dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) uid = dashboard["uid"] # Create an event and venue event = self.create_venue_and_event(self.event_name) # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # Add a panel to the dashboard self.grafana.add_panel(sensor, event) # Retrieve the current dashboard dashboard_info = self.grafana.get_dashboard_with_uid(uid) # Confirm that a panel was added to the dashboard with the expected title self.assertTrue(dashboard_info) self.assertTrue(dashboard_info["dashboard"]) self.assertTrue(dashboard_info["dashboard"]["panels"]) self.assertTrue(len(dashboard_info["dashboard"]["panels"]) == 1) self.assertTrue( dashboard_info["dashboard"]["panels"][0]["title"] == sensor.name) def test_add_multiple_panels(self): # Create a dashboard, confirm it was created and retrieve its UID dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) uid = dashboard["uid"] # Create an event and venue event = self.create_venue_and_event(self.event_name) # Create a sensor type sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() # Create 10 sensors, invoking add_panel for each new sensor for i in range(10): sensor = AGSensor.objects.create(name="".join( [self.test_sensor_name, str(i)]), type_id=sensor_type) sensor.save() self.grafana.add_panel(sensor, event) # Query dashboard to confirm 10 panels were added dashboard_info = self.grafana.get_dashboard_with_uid(uid) self.assertTrue(dashboard_info) self.assertTrue(dashboard_info["dashboard"]) self.assertTrue(dashboard_info["dashboard"]["panels"]) self.assertTrue(len(dashboard_info["dashboard"]["panels"]) == 10) # Confirm correct title for each panel for i in range(10): name = "".join([self.test_sensor_name, str(i)]) self.assertTrue( dashboard_info["dashboard"]["panels"][i]["title"] == name) def test_add_sensor_creates_panel_in_dashboard(self): # Create a dashboard, confirm it was created and retrieve its UID dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Create an event event = self.create_venue_and_event(self.event_name) # Make the event the active event AGActiveEvent.objects.create(agevent=event).save() # Create a sensor through the UI self.client.post( reverse(self.sensor_url), data={ "submit_new_sensor": "", "sensor-name": self.test_sensor["name"], "field-names": self.test_sensor["field-names"], "data-types": self.test_sensor["data-types"], "units": self.test_sensor["units"], "sensor-graph-type": self.test_sensor_graph_type, }, ) self.assertEquals(AGSensor.objects.count(), 1) # Fetch the dashboard again dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Confirm that a panel was added to the dashboard with the expected title self.assertTrue(dashboard) self.assertTrue(dashboard["dashboard"]) self.assertTrue(dashboard["dashboard"]["panels"]) self.assertTrue(len(dashboard["dashboard"]["panels"]) == 1) # Note: converting test_sensor_name to lowercase because currently # sensor names are automatically capitalized when they are created self.assertEquals( dashboard["dashboard"]["panels"][0]["title"], self.test_sensor["name"].lower(), ) def test_update_sensor_name_updates_panel_title(self): # Create a dashboard dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Create an event, create a sensor event = self.create_venue_and_event(self.event_name) # Create AGSensorType object for foreign key reference sensor_type = AGSensorType.objects.create( name=self.test_sensor["name"].lower(), processing_formula=0, format={ self.field_name_1: { "data_type": self.data_type_1, "unit": self.unit_1 }, self.field_name_2: { "data_type": self.data_type_2, "unit": self.unit_2 }, }, graph_type=self.test_sensor_graph_type, ) sensor_type.save() # Create AG Sensor Object sensor = AGSensor.objects.create(name=self.test_sensor["name"].lower(), type_id=sensor_type) sensor.save() # Add the sensor panel to the dashboard self.grafana.add_panel(sensor, event) updated_test_sensor_name = "foo" # Post Edited Name self.client.post( reverse(self.sensor_url), data={ "edit_sensor": "", "sensor-name": self.test_sensor["name"].lower(), "sensor-name-updated": updated_test_sensor_name, "field-names": self.test_sensor["field-names"], "data-types": self.test_sensor["data-types"], "units": self.test_sensor["units"], "sensor-graph-type": self.test_sensor_graph_type, }, ) # Check that AGSensor object has new name sensor = AGSensor.objects.all()[0] self.assertEqual(sensor.name, updated_test_sensor_name) # Check that sensor name was updated sensor = AGSensor.objects.all()[0] self.assertEquals(sensor.name, updated_test_sensor_name) # Confirm that Grafana panel title was updated # Confirm that a panel was added to the dashboard with the expected title dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) self.assertTrue(dashboard["dashboard"]) self.assertTrue(dashboard["dashboard"]["panels"]) self.assertTrue(len(dashboard["dashboard"]["panels"]) == 1) self.assertEquals( dashboard["dashboard"]["panels"][0]["title"], updated_test_sensor_name, ) def test_update_sensor_type_updates_panel_query(self): # Create a dashboard, confirm it was created dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Create an event event = self.create_venue_and_event(self.event_name) # Create sensor and sensor type sensor_type = AGSensorType.objects.create( name=self.test_sensor_name.lower(), processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name.lower(), type_id=sensor_type) sensor.save() # Create grafana sensor panel self.grafana.add_panel(sensor, event) # New fields field_names_updated = ["foo"] data_types_updated = ["float"] units_updated = ["km/h"] # Post edited sensor type self.client.post( reverse(self.sensor_url), data={ "edit_sensor": "", "sensor-name": self.test_sensor["name"].lower(), "sensor-name-updated": self.test_sensor["name"].lower(), "field-names": field_names_updated, "data-types": data_types_updated, "units": units_updated, "sensor-graph-type": self.test_sensor_graph_type, }, ) # Confirm that a new Grafana panel was created with a query containing all of # the current field names (not checking the full syntax of the query, just that # the new field name is in the query) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertIn( field_names_updated[0], dashboard["dashboard"]["panels"][0]["targets"][0]["rawSql"], ) def test_update_sensor_name_and_type_updates_panel_title_and_query(self): # Create a dashboard, confirm it was created dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Create an event event = self.create_venue_and_event(self.event_name) # Create sensor and sensor type sensor_type = AGSensorType.objects.create( name=self.test_sensor_name.lower(), processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name.lower(), type_id=sensor_type) sensor.save() # Create grafana sensor panel self.grafana.add_panel(sensor, event) # New fields updated_sensor_name = "bar" field_names_updated = ["foo"] data_types_updated = ["float"] units_updated = ["km/h"] # Post edited sensor type self.client.post( reverse(self.sensor_url), data={ "edit_sensor": "", "sensor-name": self.test_sensor["name"].lower(), "sensor-name-updated": updated_sensor_name, "field-names": field_names_updated, "data-types": data_types_updated, "units": units_updated, "sensor-graph-type": self.test_sensor_graph_type, }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) # Confirm title and query self.assertEquals( dashboard["dashboard"]["panels"][0]["title"], updated_sensor_name, ) self.assertIn( field_names_updated[0], dashboard["dashboard"]["panels"][0]["targets"][0]["rawSql"], ) def test_delete_sensor_deletes_panel_in_dashboard(self): # Create a dashboard, confirm it was created dashboard = self.grafana.create_dashboard(self.event_name) self.assertTrue(dashboard) # Create an event event = self.create_venue_and_event(self.event_name) sensor_type = AGSensorType.objects.create( name=self.test_sensor_name, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() self.grafana.add_panel(sensor, event) # Delete sensor self.client.post( reverse(self.delete_sensor_url, kwargs={"sensor_name": sensor.name}), follow=True, ) # Confirm sensor was deleted self.assertEquals(AGSensor.objects.count(), 0) # Confirm that sensor was deleted from Grafana # Fetch the dashboard again dashboard = self.grafana.get_dashboard_by_name(dashboard["slug"]) self.assertTrue(dashboard) # Confirm that a panel was added to the dashboard with the expected title self.assertTrue(dashboard) self.assertTrue(dashboard["dashboard"]) self.assertEquals(len(dashboard["dashboard"]["panels"]), 0) def test_add_panel_fail_no_dashboard_exists_for_event(self): # Create an event event = self.create_venue_and_event(self.event_name) # Create a sensor type and sensor sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # Check for expected ValueError and message expected_message = "Dashboard not found for this event." with self.assertRaisesMessage(ValueError, expected_message): self.grafana.add_panel(sensor, event) def test_create_postgres_datasource(self): # Create datasource self.grafana.create_postgres_datasource(self.datasource_name) # Query new datasource endpoint = os.path.join(self.grafana.endpoints["datasource_name"], self.datasource_name) headers = {"Content-Type": "application/json"} response = requests.get(url=endpoint, headers=headers, auth=("api_key", self.grafana.api_token)) # Confirm datasource exists with expected name self.assertEquals(response.json()["name"], self.datasource_name) def test_create_datasource_fail_authorization(self): self.grafana.api_token = "abcde" # invalid API token expected_message = "Invalid API key" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_postgres_datasource(self.datasource_name) def test_create_datasource_fail_permissions(self): self.grafana.api_token = self.VIEWER # API token with viewer permissions expected_message = "Access denied - check API permissions" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_postgres_datasource(self.datasource_name) def test_create_datasource_fail_empty_string(self): expected_message = "Response contains no message" with self.assertRaisesMessage(ValueError, expected_message): self.grafana.create_postgres_datasource("") def test_delete_postgres_datasource(self): # create the datasource self.grafana.create_postgres_datasource(self.datasource_name) # deleted should be True if delete_datasource_by_name returns true deleted = self.grafana.delete_datasource_by_name(self.datasource_name) self.assertTrue(deleted) # confirm that the datasource was actually deleted by querying it endpoint = os.path.join(self.grafana.endpoints["datasource_name"], self.datasource_name) headers = {"Content-Type": "application/json"} response = requests.get(url=endpoint, headers=headers, auth=("api_key", self.grafana.api_token)) self.assertTrue(response.json()["message"]) self.assertEquals(response.json()["message"], "Data source not found") def test_create_event_creates_dashboard_no_panels(self): self.grafana.create_postgres_datasource(self.datasource_name) # Create a venue venue = AGVenue.objects.create( name=self.event_name, description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() # Send a request to create an event (should trigger the creation of a # grafana dashboard of the same name) self.client.post( reverse(self.event_url), data={ "submit-event": "", "name": self.event_name, "date": self.test_event_data["date"], "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) # If there are spaces in the name, the GF API will replace them with dashes # to generate the "slug". A slug can be used to query the API. endpoint = os.path.join( self.grafana.hostname, "api/dashboards/db", self.event_name.lower().replace(" ", "-"), ) response = requests.get(url=endpoint, auth=("api_key", self.grafana.api_token)) # Confirm that a dashboard was created with the expected name self.assertEquals(response.status_code, 200) self.assertEquals(response.json()["dashboard"]["title"], self.event_name) def test_create_event_creates_dashboard_with_panel(self): self.grafana.create_postgres_datasource(self.datasource_name) # Create a venue venue = AGVenue.objects.create( name=self.test_venue_data["name"], description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() sensor_type = AGSensorType.objects.create( name=self.test_sensor_type, processing_formula=0, format=self.test_sensor_format, graph_type=self.test_sensor_graph_type, ) sensor_type.save() sensor = AGSensor.objects.create(name=self.test_sensor_name, type_id=sensor_type) sensor.save() # Send a request to create an event (should trigger the creation of a # grafana dashboard of the same name) self.client.post( reverse(self.event_url), data={ "submit-event": "", "name": self.event_name, "date": self.test_event_data["date"], "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) # If there are spaces in the name, the GF API will replace them with dashes # to generate the "slug". A slug can be used to query the API. endpoint = os.path.join( self.grafana.hostname, "api/dashboards/db", self.event_name.lower().replace(" ", "-"), ) response = requests.get(url=endpoint, auth=("api_key", self.grafana.api_token)) # Confirm that a dashboard was created with the expected name self.assertEquals(response.status_code, 200) self.assertEquals(response.json()["dashboard"]["title"], self.event_name) # Confirm that a the new dashboard can be queried and has panel with expected # title dashboard_info = self.grafana.get_dashboard_with_uid( response.json()["dashboard"]["uid"]) self.assertTrue(dashboard_info) self.assertTrue(dashboard_info["dashboard"]) self.assertTrue(dashboard_info["dashboard"]["panels"]) self.assertTrue(len(dashboard_info["dashboard"]["panels"]) == 1) self.assertTrue( dashboard_info["dashboard"]["panels"][0]["title"] == sensor.name) def test_delete_event_deletes_grafana_dashboard(self): self.grafana.create_postgres_datasource(self.datasource_name) # Create a venue venue = AGVenue.objects.create( name=self.event_name, description=self.test_venue_data["description"], latitude=self.test_venue_data["latitude"], longitude=self.test_venue_data["longitude"], ) venue.save() # Send a request to create an event (should trigger the creation of a # grafana dashboard of the same name) self.client.post( reverse(self.event_url), data={ "submit-event": "", "name": self.event_name, "date": self.test_event_data["date"], "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Retrieve event object event = AGEvent.objects.all().first() # Delete the event by posting to the delete view self.client.post( reverse(self.event_delete_url, kwargs={"event_uuid": event.uuid})) # Try and retrieve the dashboard dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertFalse(dashboard) def test_update_event_name_updates_grafana_dashboard_name(self): self.grafana.create_postgres_datasource(self.datasource_name) event = self.create_venue_and_event(self.event_name) updated_event_name = self.updated_event_name venue = AGVenue.objects.first() # Send a request to create an event (should trigger the creation of a # grafana dashboard of the same name) self.client.post( reverse(self.event_url), data={ "submit-event": "", "name": self.event_name, "date": self.test_event_data["date"], "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) dashboard = self.grafana.get_dashboard_by_name(self.event_name) self.assertTrue(dashboard) # Update the event name self.client.post( reverse(self.event_update_url, kwargs={"event_uuid": event.uuid}), data={ "name": updated_event_name, "description": self.test_event_data["description"], "venue_uuid": venue.uuid, }, ) # Confirm that a dashboard exists with the new event name dashboard = self.grafana.get_dashboard_by_name(updated_event_name) self.assertTrue(dashboard) self.assertEquals(dashboard["dashboard"]["title"], updated_event_name)
def post(self, request, *args, **kwargs): if "submit" not in request.POST: return DB = settings.DATABASES gf_host = request.POST.get("gf_host") gf_name = request.POST.get("gf_name") gf_username = request.POST.get("gf_username") gf_password = request.POST.get("gf_password") gf_token = request.POST.get("gf_token") # check whether gf_host already in use existing_host = GFConfig.objects.filter(gf_host=gf_host) if "update-config" not in request.POST and existing_host: messages.error( request, "Hostname {} already in use".format(gf_host), ) return redirect("/gfconfig") configure_grafana_github_url = os.path.join( GITHUB_DOCS_ROOT, CONFIGURE_GRAFANA_HELP_DOC ) # user providing username/pasword, generate API key automatically if not gf_token: auth_url = make_auth_url(gf_host, gf_username, gf_password) try: gf_token = Grafana.create_api_key( auth_url, "mercury-auto-admin", "Admin" ) except ValueError as error: messages.error( request, mark_safe( "Failed to create API token: {}. If this " "problem persists, please provide " "an admin API key directly with the 'Use API " "Key' option in the `Add Grafana Host` form. " 'See <a target="_blank" ' 'href="{}">Configure Grafana: How to Create an ' "API Token </a> to learn how to create an " "API " "key.".format( error, configure_grafana_github_url + "#c-how-to-create-an-api-token", ) ), ) return redirect("/gfconfig") # the user is submitting an update form with username/password if "update-config" in request.POST and gf_username and gf_password: existing_host.update( gf_username=gf_username, gf_password=gf_password, gf_token=gf_token ) messages.success(request, "Updated Grafana host: {}".format(gf_host)) return redirect("/gfconfig") # new gfconfig record config_data = GFConfig( gf_name=gf_name, gf_host=gf_host, gf_username=gf_username, gf_password=gf_password, gf_token=gf_token, gf_db_host=DB["default"]["HOST"] + ":" + str(DB["default"]["PORT"]), gf_db_name=DB["default"]["NAME"], gf_db_username=DB["default"]["USER"], gf_db_pw=DB["default"]["PASSWORD"], ) # Create Grafana instance with host and token grafana = Grafana(config_data) try: grafana.validate_credentials() except ValueError as error: messages.error(request, error) return redirect("/gfconfig") # the user is submitting an update form with a validated API key if "update-config" in request.POST: existing_host.update(gf_token=gf_token) messages.success(request, "Updated Grafana host: {}".format(gf_host)) return redirect("/gfconfig") try: grafana.create_postgres_datasource() except ValueError as error: messages.error(request, error) try: config_data.gf_current = True config_data.save() # If any events exist, add a dashboard for each event # If any sensors exist, add them to each event dashboard events = AGEvent.objects.all() sensors = AGSensor.objects.all() for event in events: grafana.create_dashboard(event.name) for sensor in sensors: grafana.add_panel(sensor, event) except ValueError as error: messages.warning(request, f"Warning: {error}") messages.success(request, "Created Grafana Host: {}".format(gf_name)) return redirect("/gfconfig")