Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Onboard command extension for Azure Managed Grafana service #4495

Merged
merged 36 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0d142ab
POC: update with command list to implement
yugangw-msft Jan 7, 2022
86a327c
consolidate control plane and dashboard commands
yugangw-msft Jan 8, 2022
f6f0398
half done: make resource group and location optional
yugangw-msft Jan 9, 2022
9f1d64a
more commands and code style fixes
yugangw-msft Jan 15, 2022
118d7c1
add a validator to workspace https://bugs.python.org/issue9334
yugangw-msft Jan 16, 2022
efb83c6
catch up a few TODOs
yugangw-msft Jan 16, 2022
a0e4c6c
Consolidate data source CRUD commands with bug fixes and samples
yugangw-msft Jan 17, 2022
00776b1
add help
yugangw-msft Jan 17, 2022
bff5d4e
bug fixes on data source commands
yugangw-msft Jan 19, 2022
8ea48c2
fix style errors
yugangw-msft Jan 22, 2022
055a128
fix bugs in data-source commands
yugangw-msft Jan 23, 2022
542bc1a
clean up command help
yugangw-msft Jan 23, 2022
fc77db4
upload whl file for tempoaray measures, before we publish it officially
yugangw-msft Jan 24, 2022
5b896a9
update readme and bug fixes
yugangw-msft Jan 24, 2022
ef61108
update get_start doc
yugangw-msft Jan 27, 2022
3cfa442
Update get_start,md
yugangw-msft Jan 25, 2022
22ecfcd
fix typos in get_start.md
yugangw-msft Jan 27, 2022
fa4679e
add vendor SDK
yugangw-msft Mar 5, 2022
3ccce00
new extension
yugangw-msft Mar 5, 2022
7e605bb
new extension built from azdev
yugangw-msft Mar 5, 2022
a439200
rename from ags to amg
yugangw-msft Mar 6, 2022
c2eda5d
add tag support
yugangw-msft Mar 6, 2022
4c97ebd
Update readme.rst
yugangw-msft Mar 6, 2022
4586d2d
undo non related changes
yugangw-msft Mar 6, 2022
f168b98
rename readme to markdown
yugangw-msft Mar 6, 2022
73a4e15
address lint error
yugangw-msft Mar 8, 2022
8b58a18
address linter error
yugangw-msft Mar 8, 2022
db45158
more fix towards command lint error
yugangw-msft Mar 8, 2022
e4d5a62
register the command module in a few common file
yugangw-msft Mar 8, 2022
5c501d8
add import
yugangw-msft Mar 11, 2022
cf46859
support gallery import
yugangw-msft Mar 12, 2022
4242517
use deep copy
yugangw-msft Mar 12, 2022
1a30dc7
address review feedback
yugangw-msft Mar 13, 2022
aa7b87e
set the minimum cli core version
yugangw-msft Mar 18, 2022
c5f3832
fix a bug in 'az grafana user show'
yugangw-msft Mar 19, 2022
8f1c5a1
Remove the 'id' on creating dashboard to prevent 'Not Found' error
yugangw-msft Mar 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
half done: make resource group and location optional
  • Loading branch information
yugangw-msft committed Mar 8, 2022
commit f6f0398c9b5aa27b7a702bba89ae474a5b1f6c60
44 changes: 28 additions & 16 deletions src/ags/azext_ags/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,39 @@ def load_arguments(self, _):
from azure.cli.core.commands.parameters import tags_type, get_three_state_flag
from azure.cli.core.commands.validators import get_default_location_from_resource_group

grafana_name_type = CLIArgumentType(options_list='--grafana-name',
help='Name of the Azure Managed Dashboard for Grafana.',
id_part='name')

with self.argument_context('grafana') as c:
c.argument('tags', tags_type)
c.argument('location', validator=get_default_location_from_resource_group)
c.argument('grafana_name', grafana_name_type, options_list=['--name', '-n'])
c.argument('uid', options_list=['--unique-identifier', '--uid'],
grafana_name_type = CLIArgumentType(options_list="--grafana-name",
help="Name of the Azure Managed Dashboard for Grafana.",
id_part="name")

with self.argument_context("grafana") as c:
c.argument("tags", tags_type)
c.argument("location", validator=get_default_location_from_resource_group)
c.argument("grafana_name", grafana_name_type, options_list=["--name", "-n"])
c.argument("uid", options_list=["--unique-identifier", "--uid"],
help=("The unique identifier (uid) of a dashboard can be used for uniquely identifying a dashboard or data source "
"between multiple Grafana installs. It’s automatically generated if not provided on creating. "
"The uid allows having consistent URLs for accessing dashboards or data sources when syncing "
"between multiple Grafana installs"))
c.argument('id', help=("The identifier (id) of a dashboard/data source is an auto-incrementing "
c.argument("id", help=("The identifier (id) of a dashboard/data source is an auto-incrementing "
"numeric value and is only unique per Grafana install."))

with self.argument_context('grafana create') as c:
c.argument('enable_system_assigned_identity', arg_type=get_three_state_flag())
with self.argument_context("grafana create") as c:
c.argument("enable_system_assigned_identity", arg_type=get_three_state_flag())

with self.argument_context('grafana dashboard') as c:
c.argument('dashboard_definition', help="The complete dashboard model in json string, or a path to a file with such json string")
with self.argument_context("grafana dashboard") as c:
c.argument("dashboard_definition", help="The complete dashboard model in json string, or a path to a file with such json string")

with self.argument_context("grafana dashboard show") as c:
c.argument("show_home_dashboard", arg_type=get_three_state_flag())

with self.argument_context("grafana data-source") as c:
c.argument("data_source", help="id, name, uid which can identify a data source")
c.argument("definition", help="json string with data source definition, or a path to a file with such content")

with self.argument_context("grafana data-source query") as c:
c.argument("conditions", nargs="+", help="space-separated condition in a format of `<name>=<value>`")
c.argument("time_from", options_list=["--from"], help="start time")
c.argument("time_to", options_list=["--to"], help="end time")
c.argument("max_data_points", help="Maximum amount of data points that dashboard panel can render")
c.argument("internal_ms", help="The time interval in milliseconds of time series")

with self.argument_context('grafana dashboard show') as c:
c.argument('show_home_dashboard', arg_type=get_three_state_flag())
31 changes: 17 additions & 14 deletions src/ags/azext_ags/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from msrestazure.tools import parse_resource_id

def example_name_or_id_validator(cmd, namespace):
# Example of a storage account name or ID validator.
# See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters
from azure.cli.core.commands.client_factory import get_subscription_id
from msrestazure.tools import is_valid_resource_id, resource_id
if namespace.storage_account:
if not is_valid_resource_id(namespace.RESOURCE):
namespace.storage_account = resource_id(
subscription=get_subscription_id(cmd.cli_ctx),
resource_group=namespace.resource_group_name,
namespace='Microsoft.Storage',
type='storageAccounts',
name=namespace.storage_account
)
from azure.cli.core.commands.validators import get_default_location_from_resource_group
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.cli.core.profiles import ResourceType

def process_grafana_create_namespace(cmd, namespace):
if not namespace.location:
get_default_location_from_resource_group(cmd, namespace)

def process_missing_resource_group_parameter(cmd, namespace):
if not namespace.resource_group_name and namespace.grafana_name:
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
resources = client.resources.list(filter="resourceType eq 'Microsoft.Dashboard/grafana'")
resources = list(resources)
match = next((i for i in resources if i.name == namespace.grafana_name), None)
if match:
namespace.resource_group_name = parse_resource_id(match.id)["resource_group"]
32 changes: 19 additions & 13 deletions src/ags/azext_ags/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from azure.cli.core.commands import CliCommandType
from azext_ags._client_factory import cf_ags

from ._validators import process_grafana_create_namespace, process_missing_resource_group_parameter

def load_command_table(self, _):

Expand All @@ -17,30 +18,35 @@ def load_command_table(self, _):

# TODO: ensure HTTP doc url in the help

# TODO ensure each command return error on 4xx/5xx

# TODO support no resource group parameter

with self.command_group('grafana', is_preview=True) as g:
g.custom_command('create', 'create_grafana')
g.custom_command('delete', 'delete_grafana')
g.custom_command('create', 'create_grafana', validator=process_grafana_create_namespace)
g.custom_command('delete', 'delete_grafana', validator=process_missing_resource_group_parameter)
g.custom_command('list', 'list_grafana')
g.custom_command('show', 'show_grafana')
g.custom_command('show', 'show_grafana', validator=process_missing_resource_group_parameter)
# g.custom_command('update', 'update_grafana')
g.custom_command('get-short-url', 'get_short_url') # TODO
# g.custom_command('get-short-url', 'get_short_url') # TODO

with self.command_group('grafana dashboard') as g:
with self.command_group('grafana dashboard', validator=process_missing_resource_group_parameter) as g:
g.custom_command('create', 'create_dashboard') # TODO need examples
g.custom_command('delete', 'delete_dashboard')
g.custom_command('list', 'list_dashboards')
g.custom_command('show', 'show_dashboard') # TODO handle HOME dashboard
g.custom_command('show', 'show_dashboard') # TODO handle HOME dashboard and name
g.custom_command('update', 'update_dashboard') # TODO
# g.custom_command('get-tags', 'get_dashboard_tags') # TODO handle HOME dashboard

#with self.command_group('grafana data-source') as g:
# g.custom_command('create', 'create_data_source') # TODO
# g.custom_command('list', 'list_data_sources')
# g.custom_command('show', 'show_data_source') # TODO handle both id, uid, name
# g.custom_command('delete', 'delete_data_source') # TODO handle both id, uid, name
# g.custom_command('query', 'query_data_source') # TODO handle both id, uid, name
# g.custom_command('test', 'test_data_source') # TODO
with self.command_group('grafana data-source') as g:
g.custom_command('create', 'create_data_source') # TODO add one more example of using service principal
g.custom_command('list', 'list_data_sources')
g.custom_command('show', 'show_data_source')
g.custom_command('delete', 'delete_data_source')
g.custom_command('query', 'query_data_source')

with self.command_group('grafana folder') as g:
pass
#with self.command_group('grafana user'):
# g.custom_command('list', 'list_users') # TODO
# g.custom_command('show', 'show_user') # TODO
Expand Down
114 changes: 73 additions & 41 deletions src/ags/azext_ags/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
def create_grafana(cmd, resource_group_name, grafana_name,
enable_system_assigned_identity=True, location=None):
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
# r = '{{"sku": {{"name": "Basic"}}, "location": "{0}"}}'.format(location)
resource = {
"sku": {
"name": "standard"
Expand All @@ -42,19 +41,19 @@ def list_grafana(cmd, resource_group_name=None):
return list(resources)


def show_grafana(cmd, resource_group_name, grafana_name):
def show_grafana(cmd, grafana_name, resource_group_name=None):
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
return client.resources.get(resource_group_name, "Microsoft.Dashboard",
"", "grafana", grafana_name, "2021-09-01-preview")


def delete_grafana(cmd, resource_group_name, grafana_name):
def delete_grafana(cmd, grafana_name, resource_group_name=None):
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
return client.resources.begin_delete(resource_group_name, "Microsoft.Dashboard",
"", "grafana", grafana_name, "2021-09-01-preview")


def show_dashboard(cmd, resource_group_name, grafana_name, uid=None, show_home_dashboard=None):
def show_dashboard(cmd, grafana_name, uid=None, show_home_dashboard=None, resource_group_name=None):
if uid:
path = "/api/dashboards/uid/" + uid
elif show_home_dashboard:
Expand All @@ -66,25 +65,21 @@ def show_dashboard(cmd, resource_group_name, grafana_name, uid=None, show_home_d
return json.loads(response.content)


def list_dashboards(cmd, resource_group_name, grafana_name):
return show_dashboard(cmd, resource_group_name, grafana_name, None)
def list_dashboards(cmd, grafana_name, resource_group_name=None):
return show_dashboard(cmd, resource_group_name=resource_group_name, grafana_name=grafana_name, show_home_dashboard=None)


def create_dashboard(cmd, resource_group_name, grafana_name, dashboard_definition):
import os
potentail_file_path = os.path.expanduser(dashboard_definition)
if os.path.exists(potentail_file_path):
from azure.cli.core.util import read_file_content
dashboard_definition = read_file_content(potentail_file_path)
def create_dashboard(cmd, grafana_name, dashboard_definition, resource_group_name):
dashboard_definition = _try_load_file_content(dashboard_definition)
response = _send_request(cmd, resource_group_name, grafana_name, "post", "/api/dashboards/db", dashboard_definition)
return json.loads(response.content)


def update_dashboard(cmd, resource_group_name, grafana_name, dashboard_definition):
return create_dashboard(cmd, resource_group_name, grafana_name, dashboard_definition)
return create_dashboard(cmd, grafana_name, dashboard_definition, resource_group_name)


def delete_dashboard(cmd, resource_group_name, grafana_name, uid):
def delete_dashboard(cmd, grafana_name, uid, resource_group_name):
_send_request(cmd, resource_group_name, grafana_name, "delete", "/api/dashboards/uid/" + uid)


Expand All @@ -93,37 +88,74 @@ def list_data_sources(cmd, resource_group_name, grafana_name):
return json.loads(response.content)


def create_data_source(cmd, resource_group_name, grafana_name, data_source_type, data_source_subscription=None, data_source_name=None):
# TODO make it easier

#data_source = {
# "access": "proxy",
# "basicAuth": False,
# # "database": "",
# # "id": 1,
# # "isDefault": false,
# "jsonData": {
# "azureAuthType": "msi",
# "subscriptionId": data_source_subscription or get_subscription_id(cmd.cli_ctx)
# },
# "name": data_source_name or data_source_type, # "Azure Monitor",
# # "orgId": 1,
# # "password": "",
# # "readOnly": false,
# "type": data_source_type, # "grafana-azure-monitor-datasource",
# # "typeLogoUrl": "public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg",
# # "typeName": "Azure Monitor",
# # "uid": "azure-monitor-oob",
# # "url": "",
# # "user": ""
#}

response = _send_request(cmd, resource_group_name, grafana_name, "post", "/api/datasources", data_source)
def show_data_source(cmd, resource_group_name, grafana_name, data_source):
return _find_data_source(cmd, resource_group_name, grafana_name, data_source)


def create_data_source(cmd, resource_group_name, grafana_name, definition):
response = _send_request(cmd, resource_group_name, grafana_name, "post", "/api/datasources", definition)
return json.loads(response.content)


def delete_data_source(cmd, resource_group_name, grafana_name, data_source):
data = _find_data_source(cmd, resource_group_name, grafana_name, data_source)
_send_request(cmd, resource_group_name, grafana_name, "delete", "/api/datasources/uid/" + data['uid'])


def query_data_source(cmd, resource_group_name, grafana_name, data_source, time_from=None, time_to=None,
max_data_points=100, internal_ms=1000, conditions=None):
if not time_from or not time_to: # TODO accept tiem string
import datetime, time
right_now = datetime.datetime.now()
if not time_from:
time_from = time.mktime((right_now - datetime.timedelta(hours=1)).timetuple()) * 1000
if not time_to:
time_to = time.mktime(right_now.timetuple()) * 1000
data_source_id = _find_data_source(cmd, resource_group_name, grafana_name, data_source)['id']

data = {
"from": time_from,
"to": time_to,
"queries":[{
"intervalMs": internal_ms,
"maxDataPoints": max_data_points,
"datasourceId": data_source_id,
"format": "time_series"
}]
}

if conditions:
for c in json.loads(conditions):
k, v = c.split('=', 1)
data['queries'][k] = v

response = _send_request(cmd, resource_group_name, grafana_name, "post", "/api/tsdb/query")
return json.loads(response.content)


def _find_data_source(cmd, resource_group_name, grafana_name, data_source):
response = _send_request(cmd, resource_group_name, grafana_name, "get", "/api/datasources/name/" + data_source)
if response.status_code >= 400:
response = _send_request(cmd, resource_group_name, grafana_name, "get", "/api/datasources/" + data_source)
if response.status_code >= 400:
response = _send_request(cmd, resource_group_name, grafana_name, "get", "/api/datasources/uid/" + data_source)
if response.status_code >= 400:
raise CLIError("Not found. Ex: {}".format(response.status_code))
return json.loads(response.content)


# For UX: we accept a file path for complex payload such as dashboard/data-source definition
def _try_load_file_content(file_content):
import os
potentail_file_path = os.path.expanduser(file_content)
if os.path.exists(potentail_file_path):
from azure.cli.core.util import read_file_content
file_content = read_file_content(potentail_file_path)
return file_content


def _send_request(cmd, resource_group_name, grafana_name, http_method, path, body=None):
grafana = show_grafana(cmd, resource_group_name, grafana_name)
grafana = show_grafana(cmd, grafana_name, resource_group_name)
endpoint = grafana.properties["endpoint"]

from azure.cli.core._profile import Profile
Expand Down
14 changes: 14 additions & 0 deletions src/ags/azext_ags/sample_data_source-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"access": "proxy",
"basicAuth": false,
"jsonData": {
"azureAuth": true,
"azureCredentials": {
"authType": "msi"
},
"azureEndpointResourceId": "https://monitor.core.windows.net"
},
"name": "Geneva Datasource 2",
"type": "geneva-datasource",
"withCredentials": false
}