Srikanth Vavilapalli | 1725e49 | 2014-12-01 17:50:52 -0800 | [diff] [blame^] | 1 | # |
| 2 | # Copyright (c) 2011,2012,2013 Big Switch Networks, Inc. |
| 3 | # |
| 4 | # Licensed under the Eclipse Public License, Version 1.0 (the |
| 5 | # "License"); you may not use this file except in compliance with the |
| 6 | # License. You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.eclipse.org/legal/epl-v10.html |
| 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 |
| 13 | # implied. See the License for the specific language governing |
| 14 | # permissions and limitations under the License. |
| 15 | # |
| 16 | |
| 17 | import error |
| 18 | import command |
| 19 | import collections |
| 20 | import numbers |
| 21 | import re |
| 22 | import traceback |
| 23 | import utif |
| 24 | import rest_to_model |
| 25 | import datetime |
| 26 | import socket |
| 27 | |
| 28 | from midw import * |
| 29 | |
| 30 | def validate_string(typedef, value): |
| 31 | """ |
| 32 | Validate a string, check length. |
| 33 | """ |
| 34 | |
| 35 | # Check to see if the typedef has any pattern restrictions. If it does, |
| 36 | # then match against all of the patterns. If it matches any |
| 37 | # of the patterns (i.e. not all of the patterns), then the argument |
| 38 | # value is considered to be valid. |
| 39 | patterns = typedef.get('pattern') |
| 40 | if patterns: |
| 41 | # If it's a single pattern convert it to a tuple, so we can handle |
| 42 | # it in a common way below. Note that we don't use collection.Sequence |
| 43 | # in the isinstance call because strings are sequences |
| 44 | if command._is_string(patterns): |
| 45 | patterns = (patterns,) |
| 46 | |
| 47 | for pattern in patterns: |
| 48 | if re.match(pattern, str(value)): |
| 49 | break |
| 50 | else: |
| 51 | command._raise_argument_validation_exception(typedef, value, |
| 52 | 'invalid string pattern') |
| 53 | |
| 54 | # Check for any length restrictions. This can be a single scalar length or |
| 55 | # a single length range or a sequence of either of those. For scalar lengths |
| 56 | # the length must match it exactly. For length ranges the length of the argument |
| 57 | # must be between the two values (inclusive). For a list of length specs, the |
| 58 | # length must match any of the lengths (either scalar or range) in the list. |
| 59 | lengths = typedef.get('length') |
| 60 | if lengths: |
| 61 | if command._is_single_range(lengths): |
| 62 | lengths = (lengths,) |
| 63 | for length in lengths: |
| 64 | command._check_one_range(length) |
| 65 | if isinstance(length, numbers.Integral): |
| 66 | if len(value) == length: |
| 67 | break |
| 68 | if len(value) >= length[0] and len(value) <= length[1]: |
| 69 | break |
| 70 | else: |
| 71 | command._raise_argument_validation_exception(typedef, value, |
| 72 | 'invalid string length') |
| 73 | |
| 74 | return value |
| 75 | |
| 76 | def validate_integer_range(typedef, value, ranges): |
| 77 | # Check for any range restrictions. This can be a single scalar value or |
| 78 | # a single range or a list/tuple of a combination of those. For scalar ranges |
| 79 | # the arg must match it exactly. For ranges the value of the argument |
| 80 | # must be between the two values (inclusive). For a list of range specs, the |
| 81 | # value must match any of the lengths (either scalar or range) in the list. |
| 82 | if ranges: |
| 83 | if command._is_single_range(ranges): |
| 84 | ranges = (ranges,) |
| 85 | for r in ranges: |
| 86 | command._check_one_range(r) |
| 87 | if isinstance(r, numbers.Integral): |
| 88 | if value == r: |
| 89 | break |
| 90 | else: |
| 91 | lower_boundary = command._convert_range_boundary(r[0], value) |
| 92 | upper_boundary = command._convert_range_boundary(r[1], value) |
| 93 | if value >= lower_boundary and value <= upper_boundary: |
| 94 | break |
| 95 | else: |
| 96 | command._raise_argument_validation_exception(typedef, value, |
| 97 | 'value is outside specified ' |
| 98 | 'range: (%d-%d)' % (r[0], r[1])) |
| 99 | |
| 100 | def validate_integer(typedef, value): |
| 101 | # Check that the value is actually an integer |
| 102 | try: |
| 103 | value = int(value) |
| 104 | except (ValueError, TypeError): |
| 105 | command._raise_argument_validation_exception(typedef, value, |
| 106 | 'value is not an integer') |
| 107 | validate_integer_range(typedef, value, typedef.get('range')) |
| 108 | |
| 109 | return value |
| 110 | |
| 111 | HEX_RE = re.compile(r'^0x[0-9a-fA-F]+$') |
| 112 | |
| 113 | def validate_hex_or_dec_integer(typedef, value): |
| 114 | # Check that the value is actually an hex or decimal integer |
| 115 | if HEX_RE.match(value): |
| 116 | # if it matches, it ought to succeed conversion, below's really not needed |
| 117 | try: |
| 118 | _value = int(value, 16) |
| 119 | except (ValueError, TypeError): |
| 120 | command._raise_argument_validation_exception(typedef, value, |
| 121 | 'value is not a hex integer') |
| 122 | else: |
| 123 | try: |
| 124 | _value = int(value) |
| 125 | except (ValueError, TypeError): |
| 126 | command._raise_argument_validation_exception(typedef, value, |
| 127 | 'value is not an integer') |
| 128 | validate_integer_range(typedef, _value, typedef.get('range')) |
| 129 | |
| 130 | return value |
| 131 | |
| 132 | |
| 133 | IP_ADDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$') |
| 134 | |
| 135 | def is_netmask(value): |
| 136 | """ |
| 137 | return True or False, True for a dotted-quad which can |
| 138 | be interpreted as a netmask, False otherwise |
| 139 | """ |
| 140 | if not IP_ADDR_RE.match(value): |
| 141 | return False |
| 142 | |
| 143 | # convert the netmask string to an integer |
| 144 | split_bytes = value.split('.') |
| 145 | netmask_int = 0 |
| 146 | for b in split_bytes: |
| 147 | netmask_int = (netmask_int << 8) + int(b) |
| 148 | |
| 149 | # Check that the value is composed of leading 1 bits followed by |
| 150 | # trailing 0 bits. |
| 151 | valid_netmasks = [0xffffffff - ((1 << i) - 1) for i in range(0,33)] |
| 152 | if netmask_int in valid_netmasks: |
| 153 | return True |
| 154 | return False |
| 155 | |
| 156 | def validate_netmask(typedef, value): |
| 157 | if not is_netmask(value): |
| 158 | command._raise_argument_validation_exception(typedef, value, |
| 159 | 'invalid netmask') |
| 160 | |
| 161 | return value |
| 162 | |
| 163 | def is_inverse_netmask(value): |
| 164 | """ |
| 165 | return True or False, True for a dotted-quad which |
| 166 | can be interpreted as inverse netmasks (like 0.0.0.255), |
| 167 | False otherwise. The full mask of (255.255.255.255) is not |
| 168 | treated as inverse_netmask, but a valid netmask. This helps |
| 169 | disambiguate inverse_netmask from regular netmask |
| 170 | """ |
| 171 | if not IP_ADDR_RE.match(value): |
| 172 | return False |
| 173 | |
| 174 | # First convert the netmask string to an integer |
| 175 | split_bytes = value.split('.') |
| 176 | netmask_int = 0 |
| 177 | for b in split_bytes: |
| 178 | netmask_int = (netmask_int << 8) + int(b) |
| 179 | |
| 180 | # Check that the value is composed of leading 0 bits followed by |
| 181 | # trailing 1 bits. To disambiguate inverse from regular netmask, |
| 182 | # we mandate a leading zero bit in the netmask |
| 183 | valid_netmasks = [((1 << i) - 1) for i in range(0,32)] |
| 184 | if netmask_int in valid_netmasks: |
| 185 | return True |
| 186 | return False |
| 187 | |
| 188 | def validate_inverse_netmask(typedef, value): |
| 189 | """ |
| 190 | These look like 0.0.0.255, and are intended to only match |
| 191 | when the there is one group of 1's and one group of zero's. |
| 192 | """ |
| 193 | if is_inverse_netmask(value): |
| 194 | return value |
| 195 | |
| 196 | command._raise_argument_validation_exception(typedef, value, |
| 197 | 'invalid netmask') |
| 198 | |
| 199 | def validate_ip_address_not_netmask(typedef, value): |
| 200 | """ |
| 201 | For ip addresses associated with interfaces, the configured value |
| 202 | must not be one of the many masks. |
| 203 | """ |
| 204 | # if its not an ip address, then its not a netmask, requiring |
| 205 | # a test for ip address match. |
| 206 | if not IP_ADDR_RE.match(value): |
| 207 | command._raise_argument_validation_exception(typedef, value, |
| 208 | 'not an ip-address') |
| 209 | |
| 210 | # must be four dotted quad's, validate each is within 2^8 range |
| 211 | if len([x for x in value.split('.') if int(x) < 256]) != 4: |
| 212 | command._raise_argument_validation_exception(typedef, value, |
| 213 | 'not an ip-address') |
| 214 | |
| 215 | if (not is_inverse_netmask(value)) and (not is_netmask(value)): |
| 216 | return value |
| 217 | |
| 218 | command._raise_argument_validation_exception(typedef, value, |
| 219 | 'must not be a mask') |
| 220 | |
| 221 | |
| 222 | CIDR_RE = re.compile(r'^((\d{1,3}\.){3}\d{1,3})/(\d{1,2}?)$') |
| 223 | |
| 224 | def validate_cidr_range(typedef, value): |
| 225 | match = CIDR_RE.match(value) |
| 226 | if not match: |
| 227 | command._raise_argument_validation_exception(typedef, value, |
| 228 | 'not cidr syntax: ip/n') |
| 229 | if int(match.group(3)) > 32: |
| 230 | command._raise_argument_validation_exception(typedef, value, |
| 231 | 'cidr range above 32') |
| 232 | |
| 233 | if len([x for x in match.group(1).split('.') if int(x) < 256]) != 4: |
| 234 | command._raise_argument_validation_exception(typedef, value, |
| 235 | 'cidr range above 32') |
| 236 | |
| 237 | return value |
| 238 | |
| 239 | |
| 240 | def validate_resolvable_ip_address(typedef, value): |
| 241 | # see if this is an object we know about, for exameple a switch. |
| 242 | dpid = convert_alias_to_object_key('switch-config', value) |
| 243 | try: |
| 244 | validate_dpid(typedef, dpid) |
| 245 | except: |
| 246 | |
| 247 | try: |
| 248 | socket.gethostbyname(value) |
| 249 | except: |
| 250 | msg = 'unresolvable name' |
| 251 | command._raise_argument_validation_exception(typedef, value, |
| 252 | msg) |
| 253 | |
| 254 | |
| 255 | |
| 256 | def validate_identifier(typedef, value, reserved = None): |
| 257 | """ |
| 258 | When identifier is used as a 'type' in the description, it will |
| 259 | validate against the sdnsh reserved_words. |
| 260 | |
| 261 | When identifier is used as a 'base-type' in the description, it |
| 262 | will also use any 'reserved' attributes in the description. |
| 263 | |
| 264 | This allows some control over whether the 'reserved' attribute |
| 265 | takes effect at the syntax-layer or at the action-layer |
| 266 | """ |
| 267 | if not re.match(r'^[a-zA-Z0-9_-]+$', str(value)): |
| 268 | msg = 'Invalid characters in identifier' |
| 269 | command._raise_argument_validation_exception(typedef, value, msg) |
| 270 | if value in sdnsh.reserved_words: |
| 271 | msg = 'reserved word "%s" in "%s"' % (value, ', '.join(sdnsh.reserved_words)) |
| 272 | command._raise_argument_validation_exception(typedef, value, msg) |
| 273 | if reserved and value in reserved: |
| 274 | msg = 'reserved word "%s" in "%s"' % (value, ', '.join(reserved)) |
| 275 | command._raise_argument_validation_exception(typedef, value, msg) |
| 276 | return value |
| 277 | |
| 278 | |
| 279 | def validate_date(typedef, value): |
| 280 | if (value == 'now' or value == 'current'): |
| 281 | return value |
| 282 | |
| 283 | try: |
| 284 | return str(int(value)) |
| 285 | except (ValueError, TypeError): |
| 286 | pass |
| 287 | |
| 288 | for f,pre in [('%Y-%m-%dT%H:%M:%S', None), |
| 289 | ('%Y-%m-%d %H:%M:%S', None), |
| 290 | ('%Y-%m-%dT%H:%M:%S%z', None), |
| 291 | ('%Y-%m-%d %H:%M:%S%z', None), |
| 292 | ('%Y-%m-%d', None), |
| 293 | ('%m-%d', '%Y-'), |
| 294 | ('%H:%M', '%Y-%m-%dT')]: |
| 295 | try: |
| 296 | t = value |
| 297 | if pre: |
| 298 | pref = datetime.datetime.now().strftime(pre) |
| 299 | f = pre + f |
| 300 | t = pref + t |
| 301 | |
| 302 | return value |
| 303 | except: |
| 304 | pass |
| 305 | |
| 306 | command._raise_argument_validation_exception(typedef, value, 'invalid date') |
| 307 | |
| 308 | |
| 309 | def validate_duration(typedef, value): |
| 310 | for s in ['weeks', 'days', 'hours', 'mins', 'secs', 'ms']: |
| 311 | if value.endswith(s): |
| 312 | return value |
| 313 | command._raise_argument_validation_exception(typedef, value, |
| 314 | 'invalid duration') |
| 315 | |
| 316 | |
| 317 | def validate_enum(typedef, value): |
| 318 | # FIXME: Sort of a hack. The base enum class doesn't have a values |
| 319 | # field, so there's nothing to check against. We really just use it |
| 320 | # as a base type to indicate the validation function (i.e. this function) |
| 321 | # to use. |
| 322 | name = typedef.get('name') |
| 323 | if name == 'enum': |
| 324 | return |
| 325 | |
| 326 | enum_values = typedef.get('values') |
| 327 | if not enum_values: |
| 328 | raise error.CommandDescriptionError('Unspecified enum values') |
| 329 | |
| 330 | if not isinstance(enum_values, collections.Mapping): |
| 331 | # If it's not a dictionary then it should be an array |
| 332 | # or tuple where the (string) elements are both the key |
| 333 | # and the value for the enum items. So here we convert |
| 334 | # to a dictionary so we can use the same logic for both |
| 335 | # cases below. |
| 336 | if command._is_string(enum_values): |
| 337 | enum_values = (enum_values,) |
| 338 | if isinstance(enum_values, collections.Sequence): |
| 339 | enum_values = dict((v,v) for v in enum_values) |
| 340 | else: |
| 341 | raise error.CommandDescriptionError( |
| 342 | 'Enum values must be either a string, dict, tuple, or list') |
| 343 | |
| 344 | prefix_matches = [] |
| 345 | lower_value = value.lower() |
| 346 | for enum_value, return_value in enum_values.items(): |
| 347 | lower_enum_value = enum_value.lower() |
| 348 | if lower_enum_value == lower_value: |
| 349 | return (return_value, enum_value) |
| 350 | if lower_enum_value.startswith(lower_value): |
| 351 | prefix_matches.append((return_value, enum_value)) |
| 352 | |
| 353 | if len(prefix_matches) == 0: |
| 354 | command._raise_argument_validation_exception(typedef, value, |
| 355 | 'unexpected value for enum', enum_values.keys()) |
| 356 | |
| 357 | return prefix_matches |
| 358 | |
| 359 | |
| 360 | COMMAND_MAC_ADDRESS_RE = re.compile(r'^(([A-Fa-f\d]){2}:?){5}[A-Fa-f\d]{2}$') |
| 361 | COMMAND_MAC_ALT_RE = re.compile(r'^(([A-Fa-f\d]){4}\.?){2}[A-Fa-f\d]{4}$') |
| 362 | |
| 363 | def _is_mac_address(value): |
| 364 | if COMMAND_MAC_ALT_RE.match(value): |
| 365 | # |
| 366 | # Convert 1234.5678.9012 into 01:34:56:78:90:12 |
| 367 | _mac = '%s:%s:%s:%s:%s:%s' % ( |
| 368 | value[0:2], value[2:4], |
| 369 | value[5:7], value[7:9], |
| 370 | value[10:12], value[12:14]) |
| 371 | return True |
| 372 | elif COMMAND_MAC_ADDRESS_RE.match(value): |
| 373 | return True |
| 374 | return False |
| 375 | |
| 376 | def validate_mac_address(typedef, value): |
| 377 | if _is_mac_address(value): |
| 378 | return value |
| 379 | command._raise_argument_validation_exception(typedef, value, 'mac address') |
| 380 | |
| 381 | def validate_host(typedef, value): |
| 382 | """ |
| 383 | """ |
| 384 | |
| 385 | if _is_mac_address(value): |
| 386 | return value |
| 387 | |
| 388 | pk = mi.pk('host-config') |
| 389 | key = convert_alias_to_object_key('host-config', value) |
| 390 | key_dict = { pk : key } |
| 391 | mi.split_compound_into_dict('host-config', pk, key_dict, is_prefix = True) |
| 392 | if 'mac' in key_dict and _is_mac_address(key_dict['mac']): |
| 393 | try: |
| 394 | _exists = rest_to_model.get_model_from_url('host', key_dict) |
| 395 | return value |
| 396 | except: |
| 397 | raise error.ArgumentValidationError( |
| 398 | 'host "%s": doesn\'t exist' % value) |
| 399 | |
| 400 | command._raise_argument_validation_exception(typedef, value, |
| 401 | 'not host alias nor mac address') |
| 402 | |
| 403 | def validate_dpid(typedef, value): |
| 404 | if not utif.COMMAND_DPID_RE.match(value): |
| 405 | command._raise_argument_validation_exception(typedef, value, 'switch dpid') |
| 406 | return value |
| 407 | |
| 408 | |
| 409 | def validate_switch_dpid(typedef, value): |
| 410 | """ |
| 411 | Either the value must be a syntactic dpid (eight-hex-bytes), |
| 412 | Or the value must be a switch alias, and the underlying dpid |
| 413 | must exist. |
| 414 | """ |
| 415 | |
| 416 | try: |
| 417 | validate_dpid(typedef, value) |
| 418 | return value |
| 419 | except error.ArgumentValidationError, e: |
| 420 | dpid = convert_alias_to_object_key('switch-config', value) |
| 421 | try: |
| 422 | validate_dpid(typedef, dpid) |
| 423 | try: |
| 424 | _exists = sdnsh.get_object_from_store('switch-config', dpid) |
| 425 | return value |
| 426 | except e: |
| 427 | command._raise_argument_validation_exception(typedef, value, |
| 428 | 'switch "%s" doesn\'t exist' % value) |
| 429 | except error.ArgumentValidationError, e: |
| 430 | command._raise_argument_validation_exception(typedef, value, |
| 431 | 'not switch alias nor dpid') |
| 432 | |
| 433 | def validate_existing_obj(typedef, value, obj_type): |
| 434 | """ |
| 435 | Lookup the specific obj_type's primary key value for validation. |
| 436 | """ |
| 437 | |
| 438 | lookup_value = convert_alias_to_object_key(obj_type, value) |
| 439 | |
| 440 | try: |
| 441 | sdnsh.get_object_from_store(obj_type, lookup_value) |
| 442 | return value |
| 443 | except Exception, _e: |
| 444 | command._raise_argument_validation_exception(typedef, value, "doesn't exist ") |
| 445 | |
| 446 | |
| 447 | def validate_config(typedef, value): |
| 448 | if value.startswith('config://'): |
| 449 | return value |
| 450 | elif utif.full_word_from_choices(value, ['running-config', |
| 451 | 'upgrade-config', |
| 452 | 'trash' ]): |
| 453 | return value |
| 454 | else: |
| 455 | for prefixes in ['http://', 'ftp://', 'tftp://', 'file://']: |
| 456 | if value.startswith(prefixes): |
| 457 | return value |
| 458 | data = sdnsh.store.get_user_data_table(value, 'latest') |
| 459 | if len(data): |
| 460 | return value |
| 461 | |
| 462 | msg = 'not a valid copy, must be (running-config, upgrade-config ' \ |
| 463 | 'or must start with config://, http://, ftp://, or tftp://' |
| 464 | command._raise_argument_validation_exception(typedef, value, msg) |
| 465 | |
| 466 | |
| 467 | def init_validations(bs, modi): |
| 468 | global sdnsh, mi |
| 469 | sdnsh = bs |
| 470 | mi = modi |
| 471 | |
| 472 | # Initialize validation functions |
| 473 | command.add_validation('validate-string', validate_string) |
| 474 | command.add_validation('validate-integer', validate_integer) |
| 475 | command.add_validation('validate-hex-or-dec-integer', validate_hex_or_dec_integer) |
| 476 | command.add_validation('validate-netmask', validate_netmask) |
| 477 | command.add_validation('validate-inverse-netmask', validate_inverse_netmask) |
| 478 | command.add_validation('validate-ip-address-not-mask', validate_ip_address_not_netmask) |
| 479 | command.add_validation('validate-cidr-range', validate_cidr_range) |
| 480 | command.add_validation('validate-resolvable-ip-address', |
| 481 | validate_resolvable_ip_address) |
| 482 | command.add_validation('validate-identifier', validate_identifier, |
| 483 | {'kwargs': {'typedef' : '$typedef', |
| 484 | 'value' : '$value', |
| 485 | 'reserved' : '$reserved'}}) |
| 486 | command.add_validation('validate-date', validate_date) |
| 487 | command.add_validation('validate-duration', validate_duration) |
| 488 | command.add_validation('validate-enum', validate_enum) |
| 489 | command.add_validation('validate-mac-address', validate_mac_address) |
| 490 | command.add_validation('validate-host', validate_host) |
| 491 | command.add_validation('validate-switch-dpid', validate_switch_dpid) |
| 492 | command.add_validation('validate-existing-obj', validate_existing_obj, |
| 493 | {'kwargs': {'typedef' : '$typedef', |
| 494 | 'value' : '$value', |
| 495 | 'obj_type' : '$obj_type'}}) |
| 496 | command.add_validation('validate-config', validate_config) |
| 497 | |