|
18 | 18 | import argparse |
19 | 19 | import getpass |
20 | 20 | import io |
| 21 | +import json |
21 | 22 | import logging |
22 | 23 | import os |
| 24 | +import urllib.parse |
23 | 25 |
|
24 | 26 | from cliff import columns as cliff_columns |
25 | 27 | import iso8601 |
@@ -681,7 +683,7 @@ def __call__(self, parser, namespace, values, option_string=None): |
681 | 683 | class BDMLegacyAction(argparse.Action): |
682 | 684 |
|
683 | 685 | def __call__(self, parser, namespace, values, option_string=None): |
684 | | - # Make sure we have an empty dict rather than None |
| 686 | + # Make sure we have an empty list rather than None |
685 | 687 | if getattr(namespace, self.dest, None) is None: |
686 | 688 | setattr(namespace, self.dest, []) |
687 | 689 |
|
@@ -723,6 +725,68 @@ def __call__(self, parser, namespace, values, option_string=None): |
723 | 725 | getattr(namespace, self.dest).append(mapping) |
724 | 726 |
|
725 | 727 |
|
| 728 | +class BDMAction(parseractions.MultiKeyValueAction): |
| 729 | + |
| 730 | + def __init__(self, option_strings, dest, **kwargs): |
| 731 | + required_keys = [] |
| 732 | + optional_keys = [ |
| 733 | + 'uuid', 'source_type', 'destination_type', |
| 734 | + 'disk_bus', 'device_type', 'device_name', 'volume_size', |
| 735 | + 'guest_format', 'boot_index', 'delete_on_termination', 'tag', |
| 736 | + 'volume_type', |
| 737 | + ] |
| 738 | + super().__init__( |
| 739 | + option_strings, dest, required_keys=required_keys, |
| 740 | + optional_keys=optional_keys, **kwargs, |
| 741 | + ) |
| 742 | + |
| 743 | + # TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b |
| 744 | + # or similar lands in a release |
| 745 | + def validate_keys(self, keys): |
| 746 | + """Validate the provided keys. |
| 747 | +
|
| 748 | + :param keys: A list of keys to validate. |
| 749 | + """ |
| 750 | + valid_keys = self.required_keys | self.optional_keys |
| 751 | + invalid_keys = [k for k in keys if k not in valid_keys] |
| 752 | + if invalid_keys: |
| 753 | + msg = _( |
| 754 | + "Invalid keys %(invalid_keys)s specified.\n" |
| 755 | + "Valid keys are: %(valid_keys)s" |
| 756 | + ) |
| 757 | + raise argparse.ArgumentTypeError(msg % { |
| 758 | + 'invalid_keys': ', '.join(invalid_keys), |
| 759 | + 'valid_keys': ', '.join(valid_keys), |
| 760 | + }) |
| 761 | + |
| 762 | + missing_keys = [k for k in self.required_keys if k not in keys] |
| 763 | + if missing_keys: |
| 764 | + msg = _( |
| 765 | + "Missing required keys %(missing_keys)s.\n" |
| 766 | + "Required keys are: %(required_keys)s" |
| 767 | + ) |
| 768 | + raise argparse.ArgumentTypeError(msg % { |
| 769 | + 'missing_keys': ', '.join(missing_keys), |
| 770 | + 'required_keys': ', '.join(self.required_keys), |
| 771 | + }) |
| 772 | + |
| 773 | + def __call__(self, parser, namespace, values, option_string=None): |
| 774 | + if getattr(namespace, self.dest, None) is None: |
| 775 | + setattr(namespace, self.dest, []) |
| 776 | + |
| 777 | + if values.startswith('file://'): |
| 778 | + path = urllib.parse.urlparse(values).path |
| 779 | + with open(path) as fh: |
| 780 | + data = json.load(fh) |
| 781 | + |
| 782 | + # Validate the keys - other validation is left to later |
| 783 | + self.validate_keys(list(data)) |
| 784 | + |
| 785 | + getattr(namespace, self.dest, []).append(data) |
| 786 | + else: |
| 787 | + super().__call__(parser, namespace, values, option_string) |
| 788 | + |
| 789 | + |
726 | 790 | class CreateServer(command.ShowOne): |
727 | 791 | _description = _("Create a new server") |
728 | 792 |
|
@@ -829,19 +893,15 @@ def get_parser(self, prog_name): |
829 | 893 | parser.add_argument( |
830 | 894 | '--block-device', |
831 | 895 | metavar='', |
832 | | - action=parseractions.MultiKeyValueAction, |
| 896 | + action=BDMAction, |
833 | 897 | dest='block_devices', |
834 | 898 | default=[], |
835 | | - required_keys=[], |
836 | | - optional_keys=[ |
837 | | - 'uuid', 'source_type', 'destination_type', |
838 | | - 'disk_bus', 'device_type', 'device_name', 'volume_size', |
839 | | - 'guest_format', 'boot_index', 'delete_on_termination', 'tag', |
840 | | - 'volume_type', |
841 | | - ], |
842 | 899 | help=_( |
843 | 900 | 'Create a block device on the server.\n' |
844 | | - 'Block device in the format:\n' |
| 901 | + 'Either a URI-style path (\'file:\\\\{path}\') to a JSON file ' |
| 902 | + 'or a CSV-serialized string describing the block device ' |
| 903 | + 'mapping.\n' |
| 904 | + 'The following keys are accepted:\n' |
845 | 905 | 'uuid=<uuid>: UUID of the volume, snapshot or ID ' |
846 | 906 | '(required if using source image, snapshot or volume),\n' |
847 | 907 | 'source_type=<source_type>: source type ' |
|
0 commit comments