1# -*- coding: utf-8 -*- #
2# Copyright 2017 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Shared flags for Cloud IoT commands."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22import enum
23
24from googlecloudsdk.api_lib.util import apis
25from googlecloudsdk.calliope import arg_parsers
26from googlecloudsdk.calliope import base
27from googlecloudsdk.command_lib.util.apis import arg_utils
28
29from six.moves import map  # pylint: disable=redefined-builtin
30
31
32def GetIdFlag(noun, action, metavar=None):
33  return base.Argument(
34      'id',
35      metavar=metavar or '{}_ID'.format(noun.replace(' ', '_').upper()),
36      help='ID of the {} {}.\n\n'.format(noun, action))
37
38
39def GetIndexFlag(noun, action):
40  return base.Argument(
41      'index',
42      type=int,
43      help='The index (zero-based) of the {} {}.'.format(noun, action))
44
45
46def AddDeviceRegistrySettingsFlagsToParser(parser, defaults=True):
47  """Get flags for device registry commands.
48
49  Args:
50    parser: argparse parser to which to add these flags.
51    defaults: bool, whether to populate default values (for instance, should be
52        false for Patch commands).
53
54  Returns:
55    list of base.Argument, the flags common to and specific to device commands.
56  """
57  base.Argument(
58      '--enable-mqtt-config',
59      help='Whether to allow MQTT connections to this device registry.',
60      default=True if defaults else None,
61      action='store_true'
62  ).AddToParser(parser)
63  base.Argument(
64      '--enable-http-config',
65      help='Whether to allow device connections to the HTTP bridge.',
66      default=True if defaults else None,
67      action='store_true'
68  ).AddToParser(parser)
69
70  base.Argument(
71      '--state-pubsub-topic',
72      required=False,
73      help=('A Google Cloud Pub/Sub topic name for state notifications.')
74  ).AddToParser(parser)
75
76  for f in _GetEventNotificationConfigFlags():
77    f.AddToParser(parser)
78
79
80def _GetEventNotificationConfigFlags():
81  """Returns a list of flags for specfiying Event Notification Configs."""
82  event_notification_spec = {
83      'topic': str,
84      'subfolder': str
85  }
86  event_config = base.Argument(
87      '--event-notification-config',
88      dest='event_notification_configs',
89      action='append',
90      required=False,
91      type=arg_parsers.ArgDict(spec=event_notification_spec,
92                               required_keys=['topic']),
93      help="""\
94The configuration for notification of telemetry events received
95from the device. This flag can be specified multiple times to add multiple
96configs to the device registry. Configs are added to the registry in the order
97the flags are specified. Only one config with an empty subfolder field is
98allowed and must be specified last.
99
100*topic*::::A Google Cloud Pub/Sub topic name for event notifications
101
102*subfolder*::::If the subfolder name matches this string exactly, this
103configuration will be used to publish telemetry events. If empty all strings
104are matched.""")
105  return [event_config]
106
107
108def AddDeviceRegistryCredentialFlagsToParser(parser, credentials_surface=True):
109  """Add device credential flags to arg parser."""
110  help_text = ('Path to a file containing an X.509v3 certificate '
111               '([RFC5280](https://www.ietf.org/rfc/rfc5280.txt)), encoded in '
112               'base64, and wrapped by `-----BEGIN CERTIFICATE-----` and '
113               '`-----END CERTIFICATE-----`.')
114  if not credentials_surface:
115    base.Argument(
116        '--public-key-path',
117        type=str,
118        help=help_text
119    ).AddToParser(parser)
120  else:
121    base.Argument(
122        '--path',
123        type=str,
124        required=True,
125        help=help_text
126    ).AddToParser(parser)
127
128
129def GetIamPolicyFileFlag():
130  return base.Argument(
131      'policy_file',
132      help='JSON or YAML file with the IAM policy')
133
134
135def AddDeviceFlagsToParser(parser, default_for_blocked_flags=True):
136  """Add flags for device commands to parser.
137
138  Args:
139    parser: argparse parser to which to add these flags.
140    default_for_blocked_flags: bool, whether to populate default values for
141        device blocked state flags.
142  """
143  for f in _GetDeviceFlags(default_for_blocked_flags):
144    f.AddToParser(parser)
145
146
147def _GetDeviceFlags(default_for_blocked_flags=True):
148  """Generates the flags for device commands."""
149  flags = []
150  blocked_state_help_text = (
151      'If blocked, connections from this device will fail.\n\n'
152      'Can be used to temporarily prevent the device from '
153      'connecting if, for example, the sensor is generating bad '
154      'data and needs maintenance.\n\n')
155
156  if not default_for_blocked_flags:
157    blocked_state_help_text += (
158        '+\n\n'  # '+' here preserves markdown indentation.
159        'Use `--no-blocked` to enable connections and `--blocked` to disable.')
160  else:
161    blocked_state_help_text += (
162        '+\n\n'
163        'Connections to device is not blocked by default.')
164
165  blocked_default = False if default_for_blocked_flags else None
166  flags.append(base.Argument(
167      '--blocked',
168      default=blocked_default,
169      action='store_true',
170      help=blocked_state_help_text))
171
172  metadata_key_validator = arg_parsers.RegexpValidator(
173      r'[a-zA-Z0-9-_]{1,127}',
174      'Invalid metadata key. Keys should only contain the following characters '
175      '[a-zA-Z0-9-_] and be fewer than 128 bytes in length.')
176  flags.append(base.Argument(
177      '--metadata',
178      metavar='KEY=VALUE',
179      type=arg_parsers.ArgDict(key_type=metadata_key_validator),
180      help="""\
181The metadata key/value pairs assigned to devices. This metadata is not
182interpreted or indexed by Cloud IoT Core. It can be used to add contextual
183information for the device.
184
185Keys should only contain the following characters ```[a-zA-Z0-9-_]``` and be
186fewer than 128 bytes in length. Values are free-form strings. Each value must
187be fewer than or equal to 32 KB in size.
188
189The total size of all keys and values must be less than 256 KB, and the
190maximum number of key-value pairs is 500.
191"""))
192
193  flags.append(base.Argument(
194      '--metadata-from-file',
195      metavar='KEY=PATH',
196      type=arg_parsers.ArgDict(key_type=metadata_key_validator),
197      help=('Same as --metadata, but the metadata values will be read from the '
198            'file specified by path.')
199  ))
200  return flags
201
202
203def AddLogLevelFlagToParser(parser):
204  choices = {
205      'none': 'Disables logging.',
206      'info': 'Informational events will be logged, such as connections and '
207              'disconnections. Also includes error events.',
208      'error': 'Error events will be logged.',
209      'debug': 'All events will be logged'
210  }
211  return base.ChoiceArgument(
212      '--log-level',
213      choices=choices,
214      help_str="""\
215      The default logging verbosity for activity from devices in this
216        registry. The verbosity level can be overridden by setting a specific
217        device's log level.
218      """).AddToParser(parser)
219
220
221class KeyTypes(enum.Enum):
222  """Valid key types for device credentials."""
223  RS256 = 1
224  ES256 = 2
225  RSA_PEM = 3
226  RSA_X509_PEM = 4
227  ES256_PEM = 5
228  ES256_X509_PEM = 6
229
230  def __init__(self, value):
231    self.choice_name = self.name.replace('_', '-').lower()
232
233
234_VALID_KEY_TYPES = {
235    KeyTypes.RSA_PEM.choice_name: """\
236        An RSA public key encoded in base64, and wrapped by
237        `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----`.
238        This can be used to verify `RS256` signatures in JWT tokens
239        ([RFC7518](https://www.ietf.org/rfc/rfc7518.txt)).""",
240    KeyTypes.RSA_X509_PEM.choice_name: """\
241        As RSA_PEM, but wrapped in an X.509v3 certificate
242        ([RFC5280](https://www.ietf.org/rfc/rfc5280.txt)),
243        encoded in base64, and wrapped by
244        `-----BEGIN CERTIFICATE-----` and
245        `-----END CERTIFICATE-----`.""",
246    KeyTypes.ES256_PEM.choice_name: """\
247        Public key for the ECDSA algorithm using P-256 and
248        SHA-256, encoded in base64, and wrapped by
249        `-----BEGIN PUBLIC KEY-----` and
250        `-----END PUBLIC KEY-----`. This can be used to verify JWT
251        tokens with the `ES256` algorithm
252        ([RFC7518](https://www.ietf.org/rfc/rfc7518.txt)). This
253        curve is defined in [OpenSSL](https://www.openssl.org/) as
254        the `prime256v1` curve.""",
255    KeyTypes.ES256_X509_PEM.choice_name: """\
256        As ES256_PEM, but wrapped in an X.509v3 certificate
257        ([RFC5280](https://www.ietf.org/rfc/rfc5280.txt)),
258        encoded in base64, and wrapped by
259        `-----BEGIN CERTIFICATE-----` and
260        `-----END CERTIFICATE-----`.""",
261    KeyTypes.RS256.choice_name: 'Deprecated name for `rsa-x509-pem`',
262    KeyTypes.ES256.choice_name: 'Deprecated nmame for `es256-pem`'
263}
264
265
266def AddDeviceCredentialFlagsToParser(parser, combine_flags=True,
267                                     only_modifiable=False):
268  """Get credentials-related flags.
269
270  Adds one of the following:
271
272    * --public-key path=PATH,type=TYPE,expiration-time=EXPIRATION_TIME
273    * --path=PATH --type=TYPE --expiration-time=EXPIRATION_TIME
274
275  depending on the value of combine_flags.
276
277  Args:
278    parser: argparse parser to which to add these flags.
279    combine_flags: bool, whether to combine these flags into one --public-key
280      flag or to leave them separate.
281    only_modifiable: bool, whether to include all flags or just those that can
282      be modified after creation.
283  """
284  for f in _GetDeviceCredentialFlags(combine_flags, only_modifiable):
285    f.AddToParser(parser)
286
287
288def _GetDeviceCredentialFlags(combine_flags=True, only_modifiable=False):
289  """"Generates credentials-related flags."""
290  flags = []
291  if not only_modifiable:
292    flags.extend([
293        base.Argument('--path', required=True, type=str,
294                      help='The path on disk to the file containing the key.'),
295        base.ChoiceArgument('--type', choices=_VALID_KEY_TYPES, required=True,
296                            help_str='The type of the key.')
297    ])
298  flags.append(
299      base.Argument('--expiration-time', type=arg_parsers.Datetime.Parse,
300                    help=('The expiration time for the key. See '
301                          '$ gcloud topic datetimes for information on '
302                          'time formats.')))
303  if not combine_flags:
304    return flags
305
306  sub_argument_help = []
307  spec = {}
308  for flag in flags:
309    name = flag.name.lstrip('-')
310    required = flag.kwargs.get('required')
311    choices = flag.kwargs.get('choices')
312    choices_str = ''
313    if choices:
314      choices_str = ', '.join(map('`{}`'.format, sorted(choices)))
315      choices_str = ' One of [{}].'.format(choices_str)
316    help_ = flag.kwargs['help']
317    spec[name] = flag.kwargs['type']
318    sub_argument_help.append(
319        '* *{name}*: {required}.{choices} {help}'.format(
320            name=name, required=('Required' if required else 'Optional'),
321            choices=choices_str, help=help_))
322  key_type_help = []
323  for key_type, description in reversed(sorted(_VALID_KEY_TYPES.items())):
324    key_type_help.append('* `{}`: {}'.format(key_type, description))
325  flag = base.Argument(
326      '--public-key',
327      dest='public_keys',
328      metavar='path=PATH,type=TYPE,[expiration-time=EXPIRATION-TIME]',
329      type=arg_parsers.ArgDict(spec=spec),
330      action='append',
331      help="""\
332Specify a public key.
333
334Supports four key types:
335
336{key_type_help}
337
338The key specification is given via the following sub-arguments:
339
340{sub_argument_help}
341
342For example:
343
344  --public-key \\
345      path=/path/to/id_rsa.pem,type=RSA_PEM,expiration-time=2017-01-01T00:00-05
346
347This flag may be provide multiple times to provide multiple keys (maximum 3).
348""".format(key_type_help='\n'.join(key_type_help),
349           sub_argument_help='\n'.join(sub_argument_help)))
350  return [flag]
351
352
353def _GetCreateFlags():
354  """Generates all the flags for the create command."""
355  return _GetDeviceFlags() + _GetDeviceCredentialFlags()
356
357
358def _GetCreateFlagsForGateways():
359  """Generates all the flags for the create command."""
360  return (_GetDeviceFlags() + _GetDeviceCredentialFlags() +
361          [CREATE_GATEWAY_ENUM_MAPPER.choice_arg,
362           GATEWAY_AUTH_METHOD_ENUM_MAPPER.choice_arg])
363
364
365def AddDeviceConfigFlagsToParser(parser):
366  """Add flags for the `configs update` command."""
367  base.Argument(
368      '--version-to-update',
369      type=int,
370      help="""\
371          The version number to update. If this value is `0` or unspecified, it
372          will not check the version number of the server and will always update
373          the current version; otherwise, this update will fail if the version
374          number provided does not match the latest version on the server. This
375          is used to detect conflicts with simultaneous updates.
376          """).AddToParser(parser)
377  data_group = parser.add_mutually_exclusive_group(required=True)
378  base.Argument(
379      '--config-file',
380      help='Path to a local file containing the data for this configuration.'
381  ).AddToParser(data_group)
382  base.Argument(
383      '--config-data',
384      help=('The data for this configuration, as a string. For any values '
385            'that contain special characters (in the context of your shell), '
386            'use the `--config-file` flag instead.')
387  ).AddToParser(data_group)
388
389
390def _GetGatewayEnum(parent='list_request'):
391  """Get GatewayTypeValueEnum from the specified parent message."""
392  messages = apis.GetMessagesModule('cloudiot', 'v1')
393  if parent == 'list_request':
394    request = (messages.CloudiotProjectsLocationsRegistriesDevicesListRequest)
395  else:
396    request = (messages.GatewayConfig)
397  return request.GatewayTypeValueValuesEnum
398
399
400def _GetAuthMethodEnum():
401  """Get GatewayAuthMethodValueValuesEnum from api messages."""
402  messages = apis.GetMessagesModule('cloudiot', 'v1')
403  return messages.GatewayConfig.GatewayAuthMethodValueValuesEnum
404
405GATEWAY_AUTH_METHOD_ENUM_MAPPER = arg_utils.ChoiceEnumMapper(
406    '--auth-method',
407    _GetAuthMethodEnum(),
408    custom_mappings={
409        'ASSOCIATION_ONLY': ('association-only',
410                             ('The device is authenticated through the '
411                              'gateway association only. Device credentials '
412                              'are ignored if provided.')),
413        'DEVICE_AUTH_TOKEN_ONLY': ('device-auth-token-only',
414                                   ('The device is authenticated through its '
415                                    'own credentials. Gateway association '
416                                    'is not checked.')),
417        'ASSOCIATION_AND_DEVICE_AUTH_TOKEN': (
418            'association-and-device-auth-token',
419            ('The device is authenticated through both device '
420             'credentials and gateway association.'))
421    },
422    required=False,
423    help_str=('The authorization/authentication method used by devices in '
424              'relation to the gateway. This property is set only on gateways. '
425              'If left unspecified, devices will not be able to access '
426              'the gateway.'))
427
428
429CREATE_GATEWAY_ENUM_MAPPER = arg_utils.ChoiceEnumMapper(
430    '--device-type',
431    _GetGatewayEnum(parent='create_request'),
432    required=False,
433    include_filter=lambda x: x != 'GATEWAY_TYPE_UNSPECIFIED',
434    help_str=('Whether this device is a gateway. If unspecified, '
435              'non-gateway is assumed. '))
436