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