diff --git a/COT/data_validation.py b/COT/data_validation.py index 610553a..1d1baaf 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -32,6 +32,7 @@ .. autosummary:: :nosignatures: + alphanum_split canonicalize_helper canonicalize_ide_subtype canonicalize_nic_subtype @@ -67,6 +68,15 @@ def to_string(obj): return str(obj) +def alphanum_split(key): + """Split the key into a list of [text, int, text, int, ...].""" + def text_to_int(text): + """Convert number strings to ints, leave other strings as text.""" + return int(text) if text.isdigit() else text + + return [text_to_int(c) for c in re.split('([0-9]+)', key)] + + def natural_sort(l): """Sort the given list "naturally" rather than in ASCII order. @@ -77,16 +87,8 @@ def natural_sort(l): :param list l: List to sort :return: Sorted list """ - def convert(text): - """Convert number strings to ints, leave other strings as text.""" - return int(text) if text.isdigit() else text - - def alphanum_key(key): - """Split the key into a list of [text, int, text, int, ...].""" - return [convert(c) for c in re.split('([0-9]+)', key)] - - # Sort based on alphanum_key - return sorted(l, key=alphanum_key) + # Sort based on alphanum_split return value + return sorted(l, key=alphanum_split) def match_or_die(first_label, first, second_label, second): diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index d74b6c6..499bed5 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -22,6 +22,7 @@ :nosignatures: expand_list_wildcard + guess_list_wildcard **Classes** @@ -38,6 +39,7 @@ import warnings from COT.data_validation import ( + alphanum_split, canonicalize_ide_subtype, canonicalize_nic_subtype, canonicalize_scsi_subtype, @@ -389,7 +391,30 @@ def _run_update_nics(self): vm = self.vm nics_dict = vm.get_nic_count(self.profiles) + max_nics = max(nics_dict.values()) if self.nics is not None: + # Special case: + # If... + # 1) We are creating at least one new NIC, AND + # 2) We didn't specify the network(s) to map the new NIC(s) to, AND + # 3) We have at least two NICs already, AND + # 4) Each existing NIC has a unique network + # ...then we will see if we can identify a pattern in the networks, + # and if so, we will create new network(s) following this pattern. + if (max_nics < self.nics and self.nic_networks is None and + max_nics >= 2 and max_nics == len(vm.networks)): + logger.info("Given that all existing NICs are mapped to " + "unique networks, trying to guess an implicit " + "pattern for creating new networks.") + # Can we guess a pattern from vm.networks? + self.nic_networks = guess_list_wildcard(vm.networks) + if self.nic_networks: + logger.info("Identified a pattern: --nic-networks %s", + " ".join(self.nic_networks)) + else: + logger.info("No pattern could be identified from %s", + vm.networks) + for (profile, count) in nics_dict.items(): if self.nics < count: self.UI.confirm_or_die( @@ -697,3 +722,52 @@ def expand_list_wildcard(name_list, length): logger.info("New list is %s", name_list) return name_list + + +def guess_list_wildcard(known_values): + """Inverse of :func:`expand_list_wildcard`. Guess the wildcard for a list. + + Examples:: + + >>> guess_list_wildcard(['foo', 'bar', 'baz']) + >>> guess_list_wildcard(['foo1', 'foo2', 'foo3']) + ['foo{1}'] + >>> guess_list_wildcard(['foo', 'bar', 'baz3', 'baz4', 'baz5']) + ['foo', 'bar', 'baz{3}'] + >>> guess_list_wildcard(['Eth0/1', 'Eth0/2', 'Eth0/3']) + ['Eth0/{1}'] + >>> guess_list_wildcard(['Eth0/0', 'Eth1/0', 'Eth2/0']) + ['Eth{0}/0'] + >>> guess_list_wildcard(['fake1', 'fake2', 'real4', 'real5']) + ['fake1', 'fake2', 'real{4}'] + + :param list known_values: Values to guess from + :return: Guessed wildcard list, or None if unable to guess + """ + logger.debug("Attempting to infer a pattern from %s", known_values) + # Guess sequences ending with simple N, N+1, N+2 + for i in range(0, len(known_values)-1): + val = known_values[i] + split_val = alphanum_split(val) + for j in range(0, len(split_val)): + candidate = split_val[j] + if not isinstance(candidate, int): + continue + prefix = "".join([str(k) for k in split_val[:j]]) + suffix = "".join([str(k) for k in split_val[j+1:]]) + logger.debug("Possible next value for %s is %s%i%s", + val, prefix, candidate+1, suffix) + possible_next = prefix + str(candidate + 1) + suffix + if known_values[i+1] == possible_next: + match_pattern = prefix + "{" + str(candidate) + "}" + suffix + logger.debug("Match pattern is %s", match_pattern) + possible_name_list = known_values[:i] + [match_pattern] + logger.debug("Checking possible name list %s", + possible_name_list) + if (expand_list_wildcard(possible_name_list, + len(known_values)) == known_values): + return possible_name_list + logger.debug("No joy") + + logger.debug("Unable to guess a pattern") + return None diff --git a/COT/tests/test_edit_hardware.py b/COT/tests/test_edit_hardware.py index 2740ec7..e411fb4 100644 --- a/COT/tests/test_edit_hardware.py +++ b/COT/tests/test_edit_hardware.py @@ -461,17 +461,75 @@ def test_set_nic_type_no_existing(self): def test_set_nic_count_add(self): """Add additional NICs across all profiles.""" + self.instance.package = self.input_ovf + self.instance.nics = 5 + self.instance.run() + self.instance.finished() + self.check_diff(""" + +- ++ + 12 +... + +- ++ + 13 +... + 13 ++ VMXNET3 ++ 10 ++ ++ ++ 14 ++ true ++ VM Network ++ VMXNET3 ethernet adapter on "VM Network"\ + ++ Ethernet4 ++ 14 ++ VMXNET3 ++ 10 ++ ++ ++ 15 ++ true ++ VM Network ++ VMXNET3 ethernet adapter on "VM Network"\ + ++ Ethernet5 ++ 15 + VMXNET3""") + + def test_set_nic_count_add_smart_networks(self): + """Add additional NICs (and implicitly networks) across all profiles. + + In this OVF, each NIC is mapped to a unique network, so COT must be + smart enough to create additional networks as well. + """ self.instance.package = self.csr_ovf self.instance.nics = 6 self.instance.run() self.instance.finished() self.check_diff(""" + Data network 3 ++ ++ ++ GigabitEthernet4 ++ ++ ++ GigabitEthernet5 ++ ++ ++ GigabitEthernet6 + +... + + 14 + true -+ GigabitEthernet3 -+ NIC representing GigabitEthernet3 ++ GigabitEthernet4 ++ NIC representing GigabitEthernet4 + GigabitEthernet4 + 14 + VMXNET3 virtio @@ -480,8 +538,8 @@ def test_set_nic_count_add(self): + + 15 + true -+ GigabitEthernet3 -+ NIC representing GigabitEthernet3 ++ GigabitEthernet5 ++ NIC representing GigabitEthernet5 + GigabitEthernet5 + 15 + VMXNET3 virtio @@ -490,8 +548,8 @@ def test_set_nic_count_add(self): + + 16 + true -+ GigabitEthernet3 -+ NIC representing GigabitEthernet3 ++ GigabitEthernet6 ++ NIC representing GigabitEthernet6 + GigabitEthernet6 + 16 + VMXNET3 virtio