diff --git a/src/command_modules/azure-cli-monitor/HISTORY.rst b/src/command_modules/azure-cli-monitor/HISTORY.rst new file mode 100644 index 00000000000..1293d8401e5 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/HISTORY.rst @@ -0,0 +1,4 @@ +.. :changelog: + +Release History +=============== diff --git a/src/command_modules/azure-cli-monitor/MANIFEST.in b/src/command_modules/azure-cli-monitor/MANIFEST.in new file mode 100644 index 00000000000..bb37a2723da --- /dev/null +++ b/src/command_modules/azure-cli-monitor/MANIFEST.in @@ -0,0 +1 @@ +include *.rst diff --git a/src/command_modules/azure-cli-monitor/README.rst b/src/command_modules/azure-cli-monitor/README.rst new file mode 100644 index 00000000000..2d463ef39b4 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/README.rst @@ -0,0 +1,3 @@ +Microsoft Azure CLI 'Monitor' Command Module +============================================ + diff --git a/src/command_modules/azure-cli-monitor/azure/__init__.py b/src/command_modules/azure-cli-monitor/azure/__init__.py new file mode 100644 index 00000000000..a9dfa5391b9 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/__init__.py @@ -0,0 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/__init__.py b/src/command_modules/azure-cli-monitor/azure/cli/__init__.py new file mode 100644 index 00000000000..a9dfa5391b9 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/__init__.py @@ -0,0 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/__init__.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/__init__.py new file mode 100644 index 00000000000..a9dfa5391b9 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/__init__.py @@ -0,0 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import pkg_resources +pkg_resources.declare_namespace(__name__) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/__init__.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/__init__.py new file mode 100644 index 00000000000..dd1eab2b197 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/__init__.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import azure.cli.command_modules.monitor.help # pylint: disable=unused-import + + +def load_params(_): + import azure.cli.command_modules.monitor.params # pylint: disable=redefined-outer-name + + +def load_commands(): + import azure.cli.command_modules.monitor.commands # pylint: disable=redefined-outer-name diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_client_factory.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_client_factory.py new file mode 100644 index 00000000000..4664711f206 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_client_factory.py @@ -0,0 +1,50 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +# MANAGEMENT CLIENT FACTORIES +def get_monitor_management_client(_): + from azure.mgmt.monitor import MonitorManagementClient + from azure.cli.core.commands.client_factory import get_mgmt_service_client + return get_mgmt_service_client(MonitorManagementClient) + + +def get_monitor_autoscale_settings_operation(kwargs): + return get_monitor_management_client(kwargs).autoscale_settings + + +def get_monitor_diagnostic_settings_operation(kwargs): + return get_monitor_management_client(kwargs).service_diagnostic_settings + + +def get_monitor_alert_rules_operation(kwargs): + return get_monitor_management_client(kwargs).alert_rules + + +def get_monitor_alert_rule_incidents_operation(kwargs): + return get_monitor_management_client(kwargs).alert_rule_incidents + + +def get_monitor_log_profiles_operation(kwargs): + return get_monitor_management_client(kwargs).log_profiles + + +# DATA CLIENT FACTORIES +def get_monitor_client(_): + from azure.monitor import MonitorClient + from azure.cli.core.commands.client_factory import get_mgmt_service_client + return get_mgmt_service_client(MonitorClient) + + +def get_monitor_activity_log_operation(kwargs): + return get_monitor_client(kwargs).activity_logs + + +def get_monitor_metric_definitions_operation(kwargs): + return get_monitor_client(kwargs).metric_definitions + + +def get_monitor_metrics_operation(kwargs): + return get_monitor_client(kwargs).metrics diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_util.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_util.py new file mode 100644 index 00000000000..bea6e57af92 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/_util.py @@ -0,0 +1,141 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.commands import register_cli_argument, cli_command +from azure.cli.core.commands.parameters import ignore_type +from azure.cli.core.commands.arm import cli_generic_update_command + + +# COMMANDS UTILITIES + +def create_service_adapter(service_model, service_class=None): + def _service_adapter(method_name): + if service_class is not None: + return '{}#{}.{}'.format(service_model, service_class, method_name) + else: + return '{}#{}'.format(service_model, method_name) + + return _service_adapter + + +# pylint: disable=too-few-public-methods +class ServiceGroup(object): + def __init__(self, scope, client_factory, service_adapter=None): + self._scope = scope + self._factory = client_factory + self._service_adapter = service_adapter or (lambda name: name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def group(self, group_name): + return CommandGroup(self._scope, group_name, self._factory, self._service_adapter) + + +class CommandGroup(object): + def __init__(self, scope, group_name, client_factory, service_adapter=None): + self._scope = scope + self._group_name = group_name + self._client_factory = client_factory + self._service_adapter = service_adapter or (lambda name: name) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def command(self, name, method_name): + cli_command(self._scope, + '{} {}'.format(self._group_name, name), + self._service_adapter(method_name), + client_factory=self._client_factory) + + def generic_update_command(self, name, getter_op, setter_op): + cli_generic_update_command( + self._scope, + '{} {}'.format(self._group_name, name), + self._service_adapter(getter_op), + self._service_adapter(setter_op), + factory=self._client_factory) + + +# PARAMETERS UTILITIES + +def patch_arg_make_required(argument): + argument.type.settings['required'] = True + + +def patch_arg_update_description(description): + def _patch_action(argument): + argument.type.settings['help'] = description + + return _patch_action + + +class ParametersContext(object): + def __init__(self, command): + self._commmand = command + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def ignore(self, argument_name): + register_cli_argument(self._commmand, argument_name, ignore_type) + + def argument(self, argument_name, arg_type=None, **kwargs): + register_cli_argument(self._commmand, argument_name, arg_type=arg_type, **kwargs) + + def register_alias(self, argument_name, options_list): + register_cli_argument(self._commmand, argument_name, options_list=options_list) + + def register(self, argument_name, options_list, **kwargs): + register_cli_argument(self._commmand, argument_name, options_list=options_list, **kwargs) + + def expand(self, argument_name, model_type, group_name=None, patches=None): + # TODO: + # two privates symbols are imported here. they should be made public or this utility class + # should be moved into azure.cli.core + from azure.cli.core.commands import _cli_extra_argument_registry + from azure.cli.core.commands._introspection import \ + (extract_args_from_signature, _option_descriptions) + + from azure.cli.command_modules.monitor.validators import get_complex_argument_processor + + if not patches: + patches = dict() + + self.ignore(argument_name) + + # fetch the documentation for model parameters first. for models, which are the classes + # derive from msrest.serialization.Model and used in the SDK API to carry parameters, the + # document of their properties are attached to the classes instead of constructors. + parameter_docs = _option_descriptions(model_type) + + expanded_arguments = [] + for name, arg in extract_args_from_signature(model_type.__init__): + if name in parameter_docs: + arg.type.settings['help'] = parameter_docs[name] + + if group_name: + arg.type.settings['arg_group'] = group_name + + if name in patches: + patches[name](arg) + + _cli_extra_argument_registry[self._commmand][name] = arg + expanded_arguments.append(name) + + self.argument(argument_name, + arg_type=ignore_type, + validator=get_complex_argument_processor(expanded_arguments, + argument_name, + model_type)) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/autoscale-parameters-template.json b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/autoscale-parameters-template.json new file mode 100644 index 00000000000..7de8a5dcb33 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/autoscale-parameters-template.json @@ -0,0 +1,38 @@ +{ + "autoscale_setting_resource_name": "{MyAutoscaleSettings}", + "location": "West US", + "tags": {}, + "profiles": [ + { + "name": "{AutoscaleProfile}", + "capacity": { + "minimum": "1", + "maximum": "5", + "default": "3" + }, + "rules": [ + { + "metric_trigger": { + "metric_name": "{name}", + "metric_resource_uri": "{FullyQualifiedAzureResourceID}", + "time_grain": "(duration in ISO8601 format)PT5M", + "statistic": "{Average|Min|Max|Sum}", + "time_window": "(duration in ISO8601 format)PT45M", + "time_aggregation": "{Average|Minimum|Maximum|Total|Count}", + "operator": "{Equals|NotEquals|GreaterThan|GreaterThanOrEqual|LessThan|LessThanOrEquals}", + "threshold": 100 + }, + "scale_action": { + "direction": "{None|Increase|Decrease}", + "type": "{ChangeCount|PercentChangeCount|ExactCount}", + "value": "2 (number of instances that are involved in the scaling)", + "cooldown": "(duration in ISO8601 format)PT20M" + } + } + ] + } + ], + "notifications": [], + "enabled": false, + "target_resource_uri": "{FullyQualifiedAzureResourceID}" +} diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/commands.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/commands.py new file mode 100644 index 00000000000..9becfb4ef70 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/commands.py @@ -0,0 +1,108 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from ._client_factory import (get_monitor_alert_rules_operation, + get_monitor_alert_rule_incidents_operation, + get_monitor_log_profiles_operation, + get_monitor_autoscale_settings_operation, + get_monitor_diagnostic_settings_operation, + get_monitor_activity_log_operation, + get_monitor_metric_definitions_operation, + get_monitor_metrics_operation) +from ._util import (ServiceGroup, create_service_adapter) + + +# MANAGEMENT COMMANDS +alert_rules_operations = create_service_adapter( + 'azure.mgmt.monitor.operations.alert_rules_operations', + 'AlertRulesOperations') + +with ServiceGroup(__name__, get_monitor_alert_rules_operation, alert_rules_operations) as s: + with s.group('monitor alert-rules') as c: + c.command('create', 'create_or_update') + c.command('delete', 'delete') + c.command('show', 'get') + c.command('list', 'list_by_resource_group') + c.generic_update_command('update', 'get', 'create_or_update') + +alert_rule_incidents_operations = create_service_adapter( + 'azure.mgmt.monitor.operations.alert_rule_incidents_operations', + 'AlertRuleIncidentsOperations') + +with ServiceGroup(__name__, get_monitor_alert_rule_incidents_operation, + alert_rule_incidents_operations) as s: + with s.group('monitor alert-rule-incidents') as c: + c.command('show', 'get') + c.command('list', 'list_by_alert_rule') + +log_profiles_operations = create_service_adapter( + 'azure.mgmt.monitor.operations.log_profiles_operations', + 'LogProfilesOperations') + +with ServiceGroup(__name__, get_monitor_log_profiles_operation, + log_profiles_operations) as s: + with s.group('monitor log-profiles') as c: + c.command('create', 'create_or_update') + c.command('delete', 'delete') + c.command('show', 'get') + c.command('list', 'list') + c.generic_update_command('update', 'get', 'create_or_update') + +diagnostic_settings_operations = create_service_adapter( + 'azure.mgmt.monitor.operations.service_diagnostic_settings_operations', + 'ServiceDiagnosticSettingsOperations') + +with ServiceGroup(__name__, get_monitor_diagnostic_settings_operation, + diagnostic_settings_operations) as s: + with s.group('monitor diagnostic-settings') as c: + c.command('create', 'create_or_update') + c.command('show', 'get') + c.generic_update_command('update', 'get', 'create_or_update') + +autoscale_settings_operations = create_service_adapter( + 'azure.mgmt.monitor.operations.autoscale_settings_operations', + 'AutoscaleSettingsOperations') + +with ServiceGroup(__name__, get_monitor_autoscale_settings_operation, + autoscale_settings_operations) as s: + with s.group('monitor autoscale-settings') as c: + c.command('create', 'create_or_update') + c.command('delete', 'delete') + c.command('show', 'get') + c.command('list', 'list_by_resource_group') + c.generic_update_command('update', 'get', 'create_or_update') + +autoscale_settings_scaffold = create_service_adapter( + 'azure.cli.command_modules.monitor.custom') + +with ServiceGroup(__name__, get_monitor_autoscale_settings_operation, + autoscale_settings_scaffold) as s: + with s.group('monitor autoscale-settings') as c: + c.command('get-parameters-template', 'scaffold_autoscale_settings_parameters') + + +# DATA COMMANDS +activity_logs_operations = create_service_adapter( + 'azure.cli.command_modules.monitor.custom') + +with ServiceGroup(__name__, get_monitor_activity_log_operation, + activity_logs_operations) as s: + with s.group('monitor activity-log') as c: + c.command('list', 'list_activity_log') + +metric_definitions_operations = create_service_adapter( + 'azure.cli.command_modules.monitor.custom') + +with ServiceGroup(__name__, get_monitor_metric_definitions_operation, + metric_definitions_operations) as s: + with s.group('monitor metric-definitions') as c: + c.command('list', 'list_metric_definitions') + +metrics_operations = create_service_adapter( + 'azure.cli.command_modules.monitor.custom') + +with ServiceGroup(__name__, get_monitor_metrics_operation, metrics_operations) as s: + with s.group('monitor metrics') as c: + c.command('list', 'list_metrics') diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/custom.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/custom.py new file mode 100644 index 00000000000..7be70a7f5d9 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/custom.py @@ -0,0 +1,243 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from __future__ import print_function +import datetime +import os +from azure.cli.core._util import get_file_json, CLIError + + +# 1 hour in milliseconds +DEFAULT_QUERY_TIME_RANGE = 3600000 + +# ISO format with explicit indication of timezone +DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + + +def list_metric_definitions(client, resource_id, metric_names=None): + '''Commands to manage metric definitions. + :param str resource_id: The identifier of the resource + :param str metric_names: The list of metric names + ''' + odata_filter = _metric_names_filter_builder(metric_names) + metric_definitions = client.list(resource_id, filter=odata_filter) + return list(metric_definitions) + + +def _metric_names_filter_builder(metric_names=None): + '''Build up OData filter string from metric_names + ''' + filters = [] + if metric_names: + for metric_name in metric_names: + filters.append("name.value eq '{}'".format(metric_name)) + return ' or '.join(filters) + + +# pylint: disable=too-many-arguments +def list_metrics(client, resource_id, time_grain, + start_time=None, end_time=None, metric_names=None): + '''Lists the metric values for a resource. + :param str resource_id: The identifier of the resource + :param str time_grain: The time grain. Granularity of the metric data returned in ISO 8601 + duration format, eg "PT1M" + :param str start_time: The start time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500. Defaults to + 1 Hour prior to the current time. + :param str end_time: The end time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500. Defaults to + current time. + :param str metric_names: The space separated list of metric names + ''' + odata_filter = _metrics_odata_filter_builder(time_grain, start_time, end_time, metric_names) + metrics = client.list(resource_id, filter=odata_filter) + return list(metrics) + + +def _metrics_odata_filter_builder(time_grain, start_time=None, end_time=None, + metric_names=None): + '''Build up OData filter string + ''' + filters = [] + metrics_filter = _metric_names_filter_builder(metric_names) + if metrics_filter: + filters.append('({})'.format(metrics_filter)) + + if time_grain: + filters.append("timeGrain eq duration'{}'".format(time_grain)) + + filters.append(_validate_time_range_and_add_defaults(start_time, end_time)) + return ' and '.join(filters) + + +def _validate_time_range_and_add_defaults(start_time, end_time, + formatter='startTime eq {} and endTime eq {}'): + end_time = _validate_end_time(end_time) + start_time = _validate_start_time(start_time, end_time) + time_range = formatter.format(start_time.strftime('%Y-%m-%dT%H:%M:%SZ'), + end_time.strftime('%Y-%m-%dT%H:%M:%SZ')) + return time_range + + +def _validate_end_time(end_time): + result_time = datetime.datetime.utcnow() + if isinstance(end_time, str): + result_time = datetime.datetime.strptime(end_time, DATE_TIME_FORMAT) + return result_time + + +def _validate_start_time(start_time, end_time): + if not isinstance(end_time, datetime.datetime): + raise ValueError("Input '{}' is not valid datetime. Valid example: 2000-12-31T12:59:59Z" + .format(end_time)) + + result_time = end_time - datetime.timedelta(seconds=DEFAULT_QUERY_TIME_RANGE) + + if isinstance(start_time, str): + result_time = datetime.datetime.strptime(start_time, DATE_TIME_FORMAT) + + now = datetime.datetime.utcnow() + if result_time > now: + raise ValueError("start_time '{}' is later than Now {}.".format(start_time, now)) + + return result_time + + +# pylint: disable=too-many-arguments +def list_activity_log(client, filters=None, correlation_id=None, resource_group=None, + resource_id=None, resource_provider=None, start_time=None, end_time=None, + caller=None, status=None, max_events=50, select=None): + '''Provides the list of activity log. + :param str filters: The OData filter for the list activity logs. If this argument is provided + OData Filter Arguments will be ignored + :param str correlation_id: The correlation id of the query + :param str resource_group: The resource group + :param str resource_id: The identifier of the resource + :param str resource_provider: The resource provider + :param str start_time: The start time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500. Defaults to + 1 Hour prior to the current time. + :param str end_time: The end time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500. Defaults to + current time. + :param str caller: The caller to look for when querying + :param str status: The status value to query (ex: Failed) + :param str max_events: The maximum number of records to be returned by the command + :param str select: The list of event names + ''' + if filters: + odata_filters = filters + else: + collection = [correlation_id, resource_group, resource_id, resource_provider] + if not _single(collection): + raise CLIError("usage error: [--correlation-id ID | --resource-group NAME | " + "--resource-id ID | --resource-provider PROVIDER]") + + odata_filters = _build_activity_log_odata_filter(correlation_id, resource_group, + resource_id, resource_provider, + start_time, end_time, + caller, status) + + if max_events: + max_events = int(max_events) + + select_filters = _activity_log_select_filter_builder(select) + activity_log = client.list(filter=odata_filters, select=select_filters) + return _limit_results(activity_log, max_events) + + +def _single(collection): + return len([x for x in collection if x]) == 1 + + +# pylint: disable=too-many-arguments +def _build_activity_log_odata_filter(correlation_id=None, resource_group=None, resource_id=None, + resource_provider=None, start_time=None, end_time=None, + caller=None, status=None): + '''Builds odata filter string. + :param str correlation_id: The correlation id of the query + :param str resource_group: The resource group + :param str resource_id: The identifier of the resource + :param str resource_provider: The resource provider + :param str start_time: The start time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500 + :param str end_time: The end time of the query. In ISO format with explicit indication of + timezone: 1970-01-01T00:00:00Z, 1970-01-01T00:00:00-0500. + :param str caller: The caller to look for when querying + :param str status: The status value to query (ex: Failed) + ''' + formatter = "eventTimestamp ge {} and eventTimestamp le {}" + odata_filters = _validate_time_range_and_add_defaults(start_time, end_time, + formatter=formatter) + + if correlation_id: + odata_filters = _build_odata_filter(odata_filters, 'correlation_id', + correlation_id, 'correlationId') + elif resource_group: + odata_filters = _build_odata_filter(odata_filters, 'resource_group', + resource_group, 'resourceGroupName') + elif resource_id: + odata_filters = _build_odata_filter(odata_filters, 'resource_id', + resource_id, 'resourceId') + elif resource_provider: + odata_filters = _build_odata_filter(odata_filters, 'resource_provider', + resource_provider, 'resourceProvider') + if caller: + odata_filters = _build_odata_filter(odata_filters, 'caller', + caller, 'caller') + if status: + odata_filters = _build_odata_filter(odata_filters, 'status', + status, 'status') + + return odata_filters + + +def _activity_log_select_filter_builder(events=None): + '''Build up select filter string from events + ''' + if events: + return ' , '.join(events) + return None + + +def _build_odata_filter(default_filter, field_name, field_value, field_label): + if not field_value: + raise CLIError('Value for {} can not be empty.'.format(field_name)) + + return _add_condition(default_filter, field_label, field_value) + + +def _add_condition(default_filter, field_label, field_value): + if not field_value: + return default_filter + + return "{} and {} eq '{}'".format(default_filter, field_label, field_value) + + +def _limit_results(paged, limit): + results = [] + for index, item in enumerate(paged): + if index < limit: + results.append(item) + else: + break + return list(results) + + +def scaffold_autoscale_settings_parameters(client): # pylint: disable=unused-argument + '''Scaffold fully formed autoscale-settings' parameters as json template + ''' + # Autoscale settings parameter scaffold file path + curr_dir = os.path.dirname(os.path.realpath(__file__)) + autoscale_settings_parameter_file_path = os.path.join( + curr_dir, 'autoscale-parameters-template.json') + + return _load_autoscale_settings_parameters(autoscale_settings_parameter_file_path) + + +def _load_autoscale_settings_parameters(file_path): + if not os.path.exists(file_path): + raise CLIError('File {} not found.'.format(file_path)) + + return get_file_json(file_path) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/help.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/help.py new file mode 100644 index 00000000000..e2bfdd2835a --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/help.py @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.help_files import helps + + +helps['monitor'] = """ + type: group + short-summary: Commands to manage Azure Monitor service. + """ +# MANAGEMENT COMMANDS HELPS +helps['monitor alert-rules'] = """ + type: group + short-summary: Commands to manage alerts assigned to Azure resources. + """ +helps['monitor alert-rules update'] = """ + type: command + short-summary: Updates an alert rule. + """ +helps['monitor alert-rule-incidents'] = """ + type: group + short-summary: Commands to manage alert rule incidents. + """ +helps['monitor log-profiles'] = """ + type: group + short-summary: Commands to manage the log profiles assigned to Azure subscription. + """ +helps['monitor log-profiles update'] = """ + type: command + short-summary: Update a log profile assigned to Azure subscription. + """ +helps['monitor diagnostic-settings'] = """ + type: group + short-summary: Commands to manage service diagnostic settings. + """ +helps['monitor diagnostic-settings update'] = """ + type: command + short-summary: Update diagnostic settings for the specified resource. + """ +helps['monitor autoscale-settings'] = """ + type: group + short-summary: Commands to manage autoscale settings. + """ +helps['monitor autoscale-settings update'] = """ + type: command + short-summary: Updates an autoscale setting. + """ +# DATA COMMANDS HELPS +helps['monitor activity-log'] = """ + type: group + short-summary: Commands to manage activity log. + """ +helps['monitor metrics'] = """ + type: group + short-summary: Commands to manage metrics. + """ +helps['monitor metric-definitions'] = """ + type: group + short-summary: Commands to manage metric definitions. + """ diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/params.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/params.py new file mode 100644 index 00000000000..7b1e4db87dd --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/params.py @@ -0,0 +1,107 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +from ._util import ParametersContext + +with ParametersContext(command='monitor alert-rules') as c: + c.register_alias('name', ('--azure-resource-name',)) + c.register_alias('rule_name', ('--name', '-n')) + +with ParametersContext(command='monitor alert-rules create') as c: + from azure.mgmt.monitor.models.alert_rule_resource import AlertRuleResource + + c.expand('parameters', AlertRuleResource) + c.register('condition', ('--condition',), + type=json.loads, + help='JSON encoded condition configuration. Use @{file} to load from a file.') + +with ParametersContext(command='monitor alert-rules show') as c: + c.argument('rule_name', id_part='name') + +with ParametersContext(command='monitor alert-rules delete') as c: + c.argument('rule_name', id_part='name') + +# https://github.com/Azure/azure-rest-api-specs/issues/1017 +with ParametersContext(command='monitor alert-rules list') as c: + c.ignore('filter') + +with ParametersContext(command='monitor alert-rule-incidents') as c: + c.register_alias('incident_name', ('--name', '-n')) + +with ParametersContext(command='monitor autoscale-settings') as c: + c.register_alias('name', ('--azure-resource-name',)) + c.register_alias('autoscale_setting_name', ('--name', '-n')) + +with ParametersContext(command='monitor autoscale-settings create') as c: + c.register('parameters', ('--parameters',), + type=json.loads, + help='JSON encoded parameters configuration. Use @{file} to load from a file.' + 'Use az autoscale-settings get-parameters-template to export json template.') + +with ParametersContext(command='monitor autoscale-settings show') as c: + c.argument('autoscale_setting_name', id_part='name') + +with ParametersContext(command='monitor autoscale-settings delete') as c: + c.argument('autoscale_setting_name', id_part='name') + +# https://github.com/Azure/azure-rest-api-specs/issues/1017 +with ParametersContext(command='monitor autoscale-settings list') as c: + c.ignore('filter') + +with ParametersContext(command='monitor diagnostic-settings') as c: + c.register_alias('resource_uri', ('--resource-id',)) + +with ParametersContext(command='monitor diagnostic-settings create') as c: + from azure.mgmt.monitor.models.service_diagnostic_settings_resource import \ + (ServiceDiagnosticSettingsResource) + + c.expand('parameters', ServiceDiagnosticSettingsResource) + c.register_alias('resource_uri', ('--resource-id',)) + c.register('logs', ('--logs',), + type=json.loads, + help='JSON encoded list of logs settings. Use @{file} to load from a file.') + c.register('metrics', ('--metrics',), + type=json.loads, + help='JSON encoded list of metric settings. Use @{file} to load from a file.') + + +with ParametersContext(command='monitor log-profiles') as c: + c.register_alias('log_profile_name', ('--name', '-n')) + +with ParametersContext(command='monitor log-profiles create') as c: + from azure.mgmt.monitor.models.log_profile_resource import LogProfileResource + from azure.mgmt.monitor.models.retention_policy import RetentionPolicy + + c.register_alias('name', ('--log-profile-name',)) + c.expand('retention_policy', RetentionPolicy) + c.expand('parameters', LogProfileResource) + c.register_alias('name', ('--log-profile-resource-name',)) + c.register_alias('log_profile_name', ('--name', '-n')) + c.argument('categories', nargs='+', + help="Space separated categories of the logs.Some values are: " + "'Write', 'Delete', and/or 'Action.'") + c.argument('locations', nargs='+', + help="Space separated list of regions for which Activity Log events " + "should be stored.") + +with ParametersContext(command='monitor metric-definitions list') as c: + c.argument('metric_names', nargs='+') + +with ParametersContext(command='monitor metrics list') as c: + c.argument('metric_names', nargs='+', required=True) + +with ParametersContext(command='monitor activity-log list') as c: + c.register_alias('resource_group', ('--resource-group', '-g')) + c.argument('select', None, nargs='+') + filter_arg_group_name = 'OData Filter' + c.argument('correlation_id', arg_group=filter_arg_group_name) + c.argument('resource_group', arg_group=filter_arg_group_name) + c.argument('resource_id', arg_group=filter_arg_group_name) + c.argument('resource_provider', arg_group=filter_arg_group_name) + c.argument('start_time', arg_group=filter_arg_group_name) + c.argument('end_time', arg_group=filter_arg_group_name) + c.argument('caller', arg_group=filter_arg_group_name) + c.argument('status', arg_group=filter_arg_group_name) diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/tests/test_custom.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/tests/test_custom.py new file mode 100644 index 00000000000..0a4ecda3e7c --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/tests/test_custom.py @@ -0,0 +1,109 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +import re +from azure.cli.core._util import CLIError +from azure.cli.command_modules.monitor.custom import (_metric_names_filter_builder, + _metrics_odata_filter_builder, + _build_activity_log_odata_filter, + _activity_log_select_filter_builder, + _build_odata_filter, + scaffold_autoscale_settings_parameters) + + +class CustomCommandTest(unittest.TestCase): + def test_metric_names_filter_builder(self): + metric_names = None + filter_output = _metric_names_filter_builder(metric_names) + assert filter_output == '' + + metric_names = ['ActionsCompleted'] + filter_output = _metric_names_filter_builder(metric_names) + assert filter_output == "name.value eq '{}'".format(metric_names[0]) + + metric_names = ['RunsSucceeded', 'ActionsCompleted'] + filter_output = _metric_names_filter_builder(metric_names) + assert filter_output == 'name.value eq \'{}\' or name.value eq \'{}\''.format( + metric_names[0], metric_names[1]) + + def test_metrics_odata_filter_builder(self): + filter_output = _metrics_odata_filter_builder('PT1M') + regex = r'^(timeGrain eq duration).*(startTime eq).*(endTime eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _metrics_odata_filter_builder('PT1M', '1970-01-01T00:00:00Z') + assert bool(re.search(regex, filter_output)) + + filter_output = _metrics_odata_filter_builder('PT1M', end_time='1970-01-01T00:00:00Z') + assert bool(re.search(regex, filter_output)) + + metric_names = ['ActionsCompleted'] + regex = r'^(\(name.value eq).*(timeGrain eq duration).*(startTime eq).*(endTime eq).*$' + filter_output = _metrics_odata_filter_builder('PT1M', metric_names=metric_names) + assert bool(re.search(regex, filter_output)) + + def test_build_activity_logs_odata_filter(self): + correlation_id = '1234-34567-56789-34567' + resource_group = 'test' + resource_id = '/subscriptions/xxxx-xxxx-xxxx-xxx/resourceGroups/testrg/providers/' \ + 'Microsoft.Web/sites/testwebapp' + resource_provider = 'Microsoft.Web' + caller = 'contoso@contoso.com' + status = 'RunsSucceeded' + + filter_output = _build_activity_log_odata_filter(correlation_id) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(correlationId eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _build_activity_log_odata_filter(resource_group=resource_group) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(resourceGroupName eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _build_activity_log_odata_filter(resource_id=resource_id) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(resourceId eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _build_activity_log_odata_filter(resource_provider=resource_provider) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(resourceProvider eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _build_activity_log_odata_filter(caller=caller) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(caller eq).*$' + assert bool(re.search(regex, filter_output)) + + filter_output = _build_activity_log_odata_filter(status=status) + regex = r'^(eventTimestamp ge).*(eventTimestamp le).*(status eq).*$' + assert bool(re.search(regex, filter_output)) + + def test_activity_logs_select_filter_builder(self): + select_output = _activity_log_select_filter_builder() + assert select_output is None + + events = ['channels'] + select_output = _activity_log_select_filter_builder(events) + assert select_output == '{}'.format(events[0]) + + events = ['eventDataId', 'eventSource'] + select_output = _activity_log_select_filter_builder(events) + assert select_output == '{} , {}'.format(events[0], events[1]) + + def test_build_odata_filter(self): + default_filter = "timeGrain eq duration'PT1M'" + field_name = 'correlation_id' + field_value = '1234-34567-56789-34567' + field_label = 'correlationId' + + filter_output = _build_odata_filter(default_filter, field_name, field_value, field_label) + regex = r'^({} and {} eq \'{}\')$'.format(default_filter, field_label, field_value) + assert bool(re.search(regex, filter_output)) + + with self.assertRaises(CLIError): + _build_odata_filter(default_filter, field_name, None, field_label) + + def test_scaffold_autoscale_settings_parameters(self): + template = scaffold_autoscale_settings_parameters(None) + if not template or not isinstance(template, dict): + assert False diff --git a/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/validators.py b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/validators.py new file mode 100644 index 00000000000..dbcea0e14f6 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/azure/cli/command_modules/monitor/validators.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def get_complex_argument_processor(expanded_arguments, assigned_arg, model_type): + """ + Return a validator which will aggregate multiple arguments to one complex argument. + """ + def _expansion_validator_impl(namespace): + """ + The validator create a argument of a given type from a specific set of arguments from CLI + command. + :param namespace: The argparse namespace represents the CLI arguments. + :return: The argument of specific type. + """ + ns = vars(namespace) + kwargs = dict((k, ns[k]) for k in ns if k in set(expanded_arguments)) + + setattr(namespace, assigned_arg, model_type(**kwargs)) + + return _expansion_validator_impl diff --git a/src/command_modules/azure-cli-monitor/setup.cfg b/src/command_modules/azure-cli-monitor/setup.cfg new file mode 100644 index 00000000000..3c6e79cf31d --- /dev/null +++ b/src/command_modules/azure-cli-monitor/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/command_modules/azure-cli-monitor/setup.py b/src/command_modules/azure-cli-monitor/setup.py new file mode 100644 index 00000000000..a65ffb11d12 --- /dev/null +++ b/src/command_modules/azure-cli-monitor/setup.py @@ -0,0 +1,55 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from codecs import open +from setuptools import setup + +VERSION = '0.0.1b1+dev' + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'License :: OSI Approved :: MIT License', +] + +DEPENDENCIES = [ + 'azure-cli-core', + 'azure-monitor==0.2.0', + 'azure-mgmt-monitor==0.1.0' +] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='azure-cli-monitor', + version=VERSION, + description='Microsoft Azure Command-Line Tools Monitor Command Module', + long_description=README + '\n\n' + HISTORY, + license='MIT', + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + url='https://github.com/Azure/azure-cli', + classifiers=CLASSIFIERS, + namespace_packages=[ + 'azure', + 'azure.cli', + 'azure.cli.command_modules' + ], + packages=[ + 'azure.cli.command_modules.monitor' + ], + install_requires=DEPENDENCIES +)