1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6from azure.cli.core.commands.validators import validate_tags, get_default_location_from_resource_group
7from azure.cli.core.azclierror import RequiredArgumentMissingError, InvalidArgumentValueError
8
9from knack.util import CLIError
10
11
12def process_autoscale_create_namespace(cmd, namespace):
13    from msrestazure.tools import parse_resource_id
14
15    validate_tags(namespace)
16    get_target_resource_validator('resource', required=True, preserve_resource_group_parameter=True)(cmd, namespace)
17    if not namespace.resource_group_name:
18        namespace.resource_group_name = parse_resource_id(namespace.resource).get('resource_group', None)
19    get_default_location_from_resource_group(cmd, namespace)
20
21
22def validate_autoscale_recurrence(namespace):
23    from azure.mgmt.monitor.models import Recurrence, RecurrentSchedule, RecurrenceFrequency
24
25    def _validate_weekly_recurrence(namespace):
26        # Construct days
27        valid_days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
28        days = []
29        for partial in namespace.recurrence[1:]:
30            if len(partial) < 2:
31                raise CLIError('specifying fewer than 2 characters for day is ambiguous.')
32            try:
33                match = next(x for x in valid_days if x.lower().startswith(partial.lower()))
34            except StopIteration:
35                raise CLIError("No match for day '{}'.".format(partial))
36            days.append(match)
37            valid_days.remove(match)
38
39        # validate, but don't process start and end time
40        recurrence_obj = Recurrence(
41            frequency=RecurrenceFrequency.week,
42            schedule=RecurrentSchedule(
43                time_zone=namespace.timezone,
44                days=days,
45                hours=[],  # will be filled in during custom command
46                minutes=[]  # will be filled in during custom command
47            )
48        )
49        return recurrence_obj
50
51    valid_recurrence = {
52        'week': {
53            'usage': '-r week [DAY DAY ...]',
54            'validator': _validate_weekly_recurrence
55        }
56    }
57    if namespace.recurrence:
58        raw_values = namespace.recurrence
59        try:
60            delimiter = raw_values[0].lower()
61            usage = valid_recurrence[delimiter]['usage']
62            try:
63                namespace.recurrence = valid_recurrence[delimiter]['validator'](namespace)
64            except CLIError as ex:
65                raise CLIError('{} invalid usage: {}'.format(ex, usage))
66        except KeyError:
67            raise CLIError('invalid usage: -r {{{}}} [ARG ARG ...]'.format(','.join(valid_recurrence)))
68
69
70def validate_autoscale_timegrain(namespace):
71    from azure.mgmt.monitor.models import MetricTrigger
72    from azure.cli.command_modules.monitor.actions import get_period_type
73    from azure.cli.command_modules.monitor.util import get_autoscale_statistic_map
74
75    values = namespace.timegrain
76    if len(values) == 1:
77        # workaround because CMD.exe eats > character... Allows condition to be
78        # specified as a quoted expression
79        values = values[0].split(' ')
80    name_offset = 0
81    try:
82        time_grain = get_period_type()(values[1])
83        name_offset += 1
84    except ValueError:
85        time_grain = get_period_type()('1m')
86    try:
87        statistic = get_autoscale_statistic_map()[values[0]]
88        name_offset += 1
89    except KeyError:
90        statistic = get_autoscale_statistic_map()['avg']
91    timegrain = MetricTrigger(
92        metric_name=None,
93        metric_resource_uri=None,
94        time_grain=time_grain,
95        statistic=statistic,
96        time_window=None,
97        time_aggregation=None,
98        operator=None,
99        threshold=None
100    )
101    namespace.timegrain = timegrain
102
103
104def get_target_resource_validator(dest, required, preserve_resource_group_parameter=False, alias='resource'):
105    def _validator(cmd, namespace):
106        from msrestazure.tools import is_valid_resource_id
107        name_or_id = getattr(namespace, dest)
108        rg = namespace.resource_group_name
109        res_ns = namespace.namespace
110        parent = namespace.parent
111        res_type = namespace.resource_type
112
113        usage_error = CLIError('usage error: --{0} ID | --{0} NAME --resource-group NAME '
114                               '--{0}-type TYPE [--{0}-parent PARENT] '
115                               '[--{0}-namespace NAMESPACE]'.format(alias))
116        if not name_or_id and required:
117            raise usage_error
118        if name_or_id:
119            if is_valid_resource_id(name_or_id) and any((res_ns, parent, res_type)):
120                raise usage_error
121            if not is_valid_resource_id(name_or_id):
122                from azure.cli.core.commands.client_factory import get_subscription_id
123                if res_type and '/' in res_type:
124                    res_ns = res_ns or res_type.rsplit('/', 1)[0]
125                    res_type = res_type.rsplit('/', 1)[1]
126                if not all((rg, res_ns, res_type, name_or_id)):
127                    raise usage_error
128
129                setattr(namespace, dest,
130                        '/subscriptions/{}/resourceGroups/{}/providers/{}/{}{}/{}'.format(
131                            get_subscription_id(cmd.cli_ctx), rg, res_ns, parent + '/' if parent else '',
132                            res_type, name_or_id))
133
134        del namespace.namespace
135        del namespace.parent
136        del namespace.resource_type
137        if not preserve_resource_group_parameter:
138            del namespace.resource_group_name
139
140    return _validator
141
142
143def validate_metrics_alert_dimension(namespace):
144    from azure.cli.command_modules.monitor.grammar.metric_alert.MetricAlertConditionValidator import dim_op_conversion
145    for keyword, value in dim_op_conversion.items():
146        if namespace.operator == value:
147            namespace.operator = keyword
148
149
150def validate_metrics_alert_condition(namespace):
151    from azure.cli.command_modules.monitor.grammar.metric_alert.MetricAlertConditionValidator import op_conversion, \
152        agg_conversion, sens_conversion
153    for keyword, value in agg_conversion.items():
154        if namespace.aggregation == value:
155            namespace.aggregation = keyword
156            break
157    for keyword, value in op_conversion.items():
158        if namespace.operator == value:
159            namespace.operator = keyword
160            break
161
162    if namespace.condition_type == 'static':
163        if namespace.threshold is None:
164            raise RequiredArgumentMissingError('Parameter --threshold is required for static threshold.')
165        if namespace.operator not in ('=', '!=', '>', '>=', '<', '<='):
166            raise InvalidArgumentValueError('Parameter --operator {} is invalid for static threshold.'.format(
167                op_conversion[namespace.operator]
168            ))
169    elif namespace.condition_type == 'dynamic':
170        if namespace.operator not in ('>', '<', '><'):
171            raise InvalidArgumentValueError('Parameter --operator {} is invalid for dynamic threshold.'.format(
172                op_conversion[namespace.operator]
173            ))
174        if namespace.alert_sensitivity is None:
175            raise RequiredArgumentMissingError('Parameter --sensitivity is required for dynamic threshold.')
176        for keyword, value in sens_conversion.items():
177            if namespace.alert_sensitivity == value:
178                namespace.alert_sensitivity = keyword
179                break
180
181        if namespace.number_of_evaluation_periods is None:
182            setattr(namespace, 'number_of_evaluation_periods', 4)
183
184        if namespace.number_of_evaluation_periods < 1 or namespace.number_of_evaluation_periods > 6:
185            raise InvalidArgumentValueError('Parameter --num-periods {} should in range 1-6.'.format(
186                namespace.number_of_evaluation_periods
187            ))
188
189        if namespace.min_failing_periods_to_alert is None:
190            setattr(namespace, 'min_failing_periods_to_alert', min(4, namespace.number_of_evaluation_periods))
191
192        if namespace.min_failing_periods_to_alert < 1 or namespace.min_failing_periods_to_alert > 6:
193            raise InvalidArgumentValueError('Parameter --num-violations {} should in range 1-6.'.format(
194                namespace.min_failing_periods_to_alert
195            ))
196
197        if namespace.min_failing_periods_to_alert > namespace.number_of_evaluation_periods:
198            raise InvalidArgumentValueError(
199                'Parameter --num-violations {} should be less than or equal to parameter --num-periods {}.'.format(
200                    namespace.min_failing_periods_to_alert, namespace.number_of_evaluation_periods))
201    else:
202        raise NotImplementedError()
203
204
205def validate_diagnostic_settings(cmd, namespace):
206    from azure.cli.core.commands.client_factory import get_subscription_id
207    from msrestazure.tools import is_valid_resource_id, resource_id, parse_resource_id
208
209    get_target_resource_validator('resource_uri', required=True, preserve_resource_group_parameter=True)(cmd, namespace)
210    if not namespace.resource_group_name:
211        namespace.resource_group_name = parse_resource_id(namespace.resource_uri)['resource_group']
212
213    if namespace.storage_account and not is_valid_resource_id(namespace.storage_account):
214        namespace.storage_account = resource_id(subscription=get_subscription_id(cmd.cli_ctx),
215                                                resource_group=namespace.resource_group_name,
216                                                namespace='microsoft.Storage',
217                                                type='storageAccounts',
218                                                name=namespace.storage_account)
219
220    if namespace.workspace and not is_valid_resource_id(namespace.workspace):
221        namespace.workspace = resource_id(subscription=get_subscription_id(cmd.cli_ctx),
222                                          resource_group=namespace.resource_group_name,
223                                          namespace='microsoft.OperationalInsights',
224                                          type='workspaces',
225                                          name=namespace.workspace)
226
227    if namespace.event_hub and is_valid_resource_id(namespace.event_hub):
228        namespace.event_hub = parse_resource_id(namespace.event_hub)['name']
229
230    if namespace.event_hub_rule:
231        if not is_valid_resource_id(namespace.event_hub_rule):
232            if not namespace.event_hub:
233                raise CLIError('usage error: --event-hub-rule ID | --event-hub-rule NAME --event-hub NAME')
234            # use value from --event-hub if the rule is a name
235            namespace.event_hub_rule = resource_id(
236                subscription=get_subscription_id(cmd.cli_ctx),
237                resource_group=namespace.resource_group_name,
238                namespace='Microsoft.EventHub',
239                type='namespaces',
240                name=namespace.event_hub,
241                child_type_1='AuthorizationRules',
242                child_name_1=namespace.event_hub_rule)
243        elif not namespace.event_hub:
244            # extract the event hub name from `--event-hub-rule` if provided as an ID
245            namespace.event_hub = parse_resource_id(namespace.event_hub_rule)['name']
246
247    if not any([namespace.storage_account, namespace.workspace, namespace.event_hub]):
248        raise CLIError(
249            'usage error - expected one or more:  --storage-account NAME_OR_ID | --workspace NAME_OR_ID '
250            '| --event-hub NAME_OR_ID | --event-hub-rule ID')
251
252    try:
253        del namespace.resource_group_name
254    except AttributeError:
255        pass
256
257
258def _validate_tags(namespace):
259    """ Extracts multiple space-separated tags in key[=value] format """
260    if isinstance(namespace.tags, list):
261        tags_dict = {}
262        for item in namespace.tags:
263            tags_dict.update(_validate_tag(item))
264        namespace.tags = tags_dict
265
266
267def _validate_tag(string):
268    """ Extracts a single tag in key[=value] format """
269    result = {}
270    if string:
271        comps = string.split('=', 1)
272        result = {comps[0]: comps[1]} if len(comps) > 1 else {string: ''}
273    return result
274
275
276def process_action_group_detail_for_creation(namespace):
277    from azure.mgmt.monitor.models import ActionGroupResource, EmailReceiver, SmsReceiver, WebhookReceiver, \
278        ArmRoleReceiver, AzureAppPushReceiver, ItsmReceiver, AutomationRunbookReceiver, \
279        VoiceReceiver, LogicAppReceiver, AzureFunctionReceiver
280
281    _validate_tags(namespace)
282
283    ns = vars(namespace)
284    name = ns['action_group_name']
285    receivers = ns.pop('receivers') or []
286    action_group_resource_properties = {
287        'location': 'global',  # as of now, 'global' is the only available location for action group
288        'group_short_name': ns.pop('short_name') or name[:12],  # '12' is the short name length limitation
289        'email_receivers': [r for r in receivers if isinstance(r, EmailReceiver)],
290        'sms_receivers': [r for r in receivers if isinstance(r, SmsReceiver)],
291        'webhook_receivers': [r for r in receivers if isinstance(r, WebhookReceiver)],
292        'arm_role_receivers': [r for r in receivers if isinstance(r, ArmRoleReceiver)],
293        'itsm_receivers': [r for r in receivers if isinstance(r, ItsmReceiver)],
294        'azure_app_push_receivers': [r for r in receivers if isinstance(r, AzureAppPushReceiver)],
295        'automation_runbook_receivers': [r for r in receivers if isinstance(r, AutomationRunbookReceiver)],
296        'voice_receivers': [r for r in receivers if isinstance(r, VoiceReceiver)],
297        'logic_app_receivers': [r for r in receivers if isinstance(r, LogicAppReceiver)],
298        'azure_function_receivers': [r for r in receivers if isinstance(r, AzureFunctionReceiver)],
299        'tags': ns.get('tags') or None
300    }
301    if hasattr(namespace, 'tags'):
302        del namespace.tags
303
304    ns['action_group'] = ActionGroupResource(**action_group_resource_properties)
305
306
307def validate_metric_dimension(namespace):
308
309    if not namespace.dimension:
310        return
311
312    if namespace.filters:
313        raise CLIError('usage: --dimension and --filter parameters are mutually exclusive.')
314
315    namespace.filters = ' and '.join("{} eq '*'".format(d) for d in namespace.dimension)
316
317
318def process_webhook_prop(namespace):
319    if not isinstance(namespace.webhook_properties, list):
320        return
321
322    result = {}
323    for each in namespace.webhook_properties:
324        if each:
325            if '=' in each:
326                key, value = each.split('=', 1)
327            else:
328                key, value = each, ''
329            result[key] = value
330
331    namespace.webhook_properties = result
332
333
334def get_action_group_validator(dest):
335    def validate_action_groups(cmd, namespace):
336        action_groups = getattr(namespace, dest, None)
337
338        if not action_groups:
339            return
340
341        from msrestazure.tools import is_valid_resource_id, resource_id
342        from azure.cli.core.commands.client_factory import get_subscription_id
343
344        subscription = get_subscription_id(cmd.cli_ctx)
345        resource_group = namespace.resource_group_name
346        for group in action_groups:
347            if not is_valid_resource_id(group.action_group_id):
348                group.action_group_id = resource_id(
349                    subscription=subscription,
350                    resource_group=resource_group,
351                    namespace='microsoft.insights',
352                    type='actionGroups',
353                    name=group.action_group_id
354                )
355    return validate_action_groups
356
357
358def get_action_group_id_validator(dest):
359    def validate_action_group_ids(cmd, namespace):
360        action_groups = getattr(namespace, dest, None)
361
362        if not action_groups:
363            return
364
365        from msrestazure.tools import is_valid_resource_id, resource_id
366        from azure.cli.core.commands.client_factory import get_subscription_id
367
368        action_group_ids = []
369        subscription = get_subscription_id(cmd.cli_ctx)
370        resource_group = namespace.resource_group_name
371        for group in action_groups:
372            if not is_valid_resource_id(group):
373                group = resource_id(
374                    subscription=subscription,
375                    resource_group=resource_group,
376                    namespace='microsoft.insights',
377                    type='actionGroups',
378                    name=group
379                )
380            action_group_ids.append(group.lower())
381        setattr(namespace, dest, action_group_ids)
382    return validate_action_group_ids
383
384
385def validate_private_endpoint_connection_id(namespace):
386    if namespace.connection_id:
387        from azure.cli.core.util import parse_proxy_resource_id
388        result = parse_proxy_resource_id(namespace.connection_id)
389        namespace.resource_group_name = result['resource_group']
390        namespace.scope_name = result['name']
391        namespace.private_endpoint_connection_name = result['child_name_1']
392
393    if not all([namespace.scope_name, namespace.resource_group_name, namespace.private_endpoint_connection_name]):
394        raise CLIError('incorrect usage. Please provide [--id ID] or [--name NAME --scope-name NAME -g NAME]')
395
396    del namespace.connection_id
397
398
399def validate_storage_accounts_name_or_id(cmd, namespace):
400    if namespace.storage_account_ids:
401        from msrestazure.tools import is_valid_resource_id, resource_id
402        from azure.cli.core.commands.client_factory import get_subscription_id
403        for index, storage_account_id in enumerate(namespace.storage_account_ids):
404            if not is_valid_resource_id(storage_account_id):
405                namespace.storage_account_ids[index] = resource_id(
406                    subscription=get_subscription_id(cmd.cli_ctx),
407                    resource_group=namespace.resource_group_name,
408                    namespace='Microsoft.Storage',
409                    type='storageAccounts',
410                    name=storage_account_id
411                )
412
413
414def process_subscription_id(cmd, namespace):
415    from azure.cli.core.commands.client_factory import get_subscription_id
416    namespace.subscription_id = get_subscription_id(cmd.cli_ctx)
417
418
419def process_workspace_data_export_destination(namespace):
420    if namespace.destination:
421        from azure.mgmt.core.tools import is_valid_resource_id, resource_id, parse_resource_id
422        if not is_valid_resource_id(namespace.destination):
423            raise CLIError('usage error: --destination should be a storage account, '
424                           'an evenhug namespace or an event hub resource id.')
425        result = parse_resource_id(namespace.destination)
426        if result['namespace'].lower() == 'microsoft.storage' and result['type'].lower() == 'storageaccounts':
427            namespace.data_export_type = 'StorageAccount'
428        elif result['namespace'].lower() == 'microsoft.eventhub' and result['type'].lower() == 'namespaces':
429            namespace.data_export_type = 'EventHub'
430            namespace.destination = resource_id(
431                subscription=result['subscription'],
432                resource_group=result['resource_group'],
433                namespace=result['namespace'],
434                type=result['type'],
435                name=result['name']
436            )
437            if 'child_type_1' in result and result['child_type_1'].lower() == 'eventhubs':
438                namespace.event_hub_name = result['child_name_1']
439        else:
440            raise CLIError('usage error: --destination should be a storage account, '
441                           'an evenhug namespace or an event hub resource id.')
442