From 2bad9a6c11d6ac4353dbecffff69bed6f593166a Mon Sep 17 00:00:00 2001 From: Nathan Johnson Date: Sat, 16 Sep 2017 01:46:42 -0500 Subject: [PATCH] CLOUDSTACK-9949: add ability to specify mac address (#2143) Added ability to specify mac in deployVirtualMachine and addNicToVirtualMachine api endpoints. Validates mac address to be in the form of: aa:bb:cc:dd:ee:ff , aa-bb-cc-dd-ee-ff , or aa.bb.cc.dd.ee.ff. Ensures that mac address is a Unicast mac. Ensures that the mac address is not already allocated for the specified network. --- api/src/com/cloud/network/Network.java | 14 +++++++ api/src/com/cloud/network/NetworkModel.java | 3 +- api/src/com/cloud/vm/NicProfile.java | 5 +++ .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/command/user/vm/AddNicToVMCmd.java | 17 +++++++++ .../api/command/user/vm/DeployVMCmd.java | 29 +++++++++++++- .../schema/src/com/cloud/vm/dao/NicDao.java | 2 + .../src/com/cloud/vm/dao/NicDaoImpl.java | 9 +++++ .../cloud/network/IpAddressManagerImpl.java | 13 +++++-- .../com/cloud/network/NetworkModelImpl.java | 15 +++++++- .../src/com/cloud/vm/UserVmManagerImpl.java | 19 +++++++--- .../cloud/network/MockNetworkModelImpl.java | 3 +- .../com/cloud/vpc/MockNetworkModelImpl.java | 3 +- test/integration/smoke/test_nic.py | 38 +++++++++++++++++++ tools/marvin/marvin/lib/base.py | 12 +++++- .../java/com/cloud/utils/net/NetUtils.java | 27 +++++++++++++ .../com/cloud/utils/net/NetUtilsTest.java | 18 +++++++++ 17 files changed, 210 insertions(+), 18 deletions(-) diff --git a/api/src/com/cloud/network/Network.java b/api/src/com/cloud/network/Network.java index 122ce708327..2cd41494244 100644 --- a/api/src/com/cloud/network/Network.java +++ b/api/src/com/cloud/network/Network.java @@ -277,12 +277,26 @@ public interface Network extends ControlledEntity, StateObject, I public class IpAddresses { private String ip4Address; private String ip6Address; + private String macAddress; + + public String getMacAddress() { + return macAddress; + } + + public void setMacAddress(String macAddress) { + this.macAddress = macAddress; + } public IpAddresses(String ip4Address, String ip6Address) { setIp4Address(ip4Address); setIp6Address(ip6Address); } + public IpAddresses(String ipAddress, String ip6Address, String macAddress) { + this(ipAddress, ip6Address); + setMacAddress(macAddress); + } + public String getIp4Address() { return ip4Address; } diff --git a/api/src/com/cloud/network/NetworkModel.java b/api/src/com/cloud/network/NetworkModel.java index 4a09ea7d140..220fa99467f 100644 --- a/api/src/com/cloud/network/NetworkModel.java +++ b/api/src/com/cloud/network/NetworkModel.java @@ -27,6 +27,7 @@ import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network.Capability; +import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.Networks.TrafficType; @@ -260,7 +261,7 @@ public interface NetworkModel { void checkIp6Parameters(String startIPv6, String endIPv6, String ip6Gateway, String ip6Cidr) throws InvalidParameterValueException; - void checkRequestedIpAddresses(long networkId, String ip4, String ip6) throws InvalidParameterValueException; + void checkRequestedIpAddresses(long networkId, IpAddresses ips) throws InvalidParameterValueException; String getStartIpv6Address(long id); diff --git a/api/src/com/cloud/vm/NicProfile.java b/api/src/com/cloud/vm/NicProfile.java index 58e05124c89..6ef9cfe5db1 100644 --- a/api/src/com/cloud/vm/NicProfile.java +++ b/api/src/com/cloud/vm/NicProfile.java @@ -114,6 +114,11 @@ public class NicProfile implements InternalIdentity, Serializable { this.requestedIPv6 = requestedIPv6; } + public NicProfile(String requestedIPv4, String requestedIPv6, String requestedMacAddress) { + this(requestedIPv4, requestedIPv6); + this.macAddress = requestedMacAddress; + } + public NicProfile(ReservationStrategy strategy, String iPv4Address, String macAddress, String iPv4gateway, String iPv4netmask) { format = AddressFormat.Ip4; this.iPv4Address = iPv4Address; diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 88ade5c1656..0a8a112d737 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -172,6 +172,7 @@ public class ApiConstants { public static final String LUN = "lun"; public static final String LBID = "lbruleid"; public static final String MAX = "max"; + public static final String MAC_ADDRESS = "macaddress"; public static final String MAX_SNAPS = "maxsnaps"; public static final String MEMORY = "memory"; public static final String MODE = "mode"; diff --git a/api/src/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java b/api/src/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java index f265ecf236a..ef03e78bea5 100644 --- a/api/src/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java @@ -36,8 +36,10 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; import com.cloud.uservm.UserVm; +import com.cloud.utils.net.NetUtils; import com.cloud.vm.VirtualMachine; @APICommand(name = "addNicToVirtualMachine", description = "Adds VM to specified network by creating a NIC", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, @@ -60,6 +62,9 @@ public class AddNicToVMCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, description = "IP Address for the new network") private String ipaddr; + @Parameter(name = ApiConstants.MAC_ADDRESS, type = CommandType.STRING, description = "Mac Address for the new network") + private String macaddr; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -76,6 +81,18 @@ public class AddNicToVMCmd extends BaseAsyncCmd { return ipaddr; } + public String getMacAddress() { + if (macaddr == null) { + return null; + } + if(!NetUtils.isValidMac(macaddr)) { + throw new InvalidParameterValueException("Mac address is not valid: " + macaddr); + } else if(!NetUtils.isUnicastMac(macaddr)) { + throw new InvalidParameterValueException("Mac address is not unicast: " + macaddr); + } + return NetUtils.standardizeMacAddress(macaddr); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index da0ce2a2ff9..bd2ae6f9c17 100644 --- a/api/src/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -147,7 +147,7 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG private List securityGroupNameList; @Parameter(name = ApiConstants.IP_NETWORK_LIST, type = CommandType.MAP, description = "ip to network mapping. Can't be specified with networkIds parameter." - + " Example: iptonetworklist[0].ip=10.10.10.11&iptonetworklist[0].ipv6=fc00:1234:5678::abcd&iptonetworklist[0].networkid=uuid - requests to use ip 10.10.10.11 in network id=uuid") + + " Example: iptonetworklist[0].ip=10.10.10.11&iptonetworklist[0].ipv6=fc00:1234:5678::abcd&iptonetworklist[0].networkid=uuid&iptonetworklist[0].mac=aa:bb:cc:dd:ee::ff - requests to use ip 10.10.10.11 in network id=uuid") private Map ipToNetworkList; @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, description = "the ip address for default vm's network") @@ -156,6 +156,9 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG @Parameter(name = ApiConstants.IP6_ADDRESS, type = CommandType.STRING, description = "the ipv6 address for default vm's network") private String ip6Address; + @Parameter(name = ApiConstants.MAC_ADDRESS, type = CommandType.STRING, description = "the mac address for default vm's network") + private String macAddress; + @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") private String keyboard; @@ -333,10 +336,19 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG } String requestedIp = ips.get("ip"); String requestedIpv6 = ips.get("ipv6"); + String requestedMac = ips.get("mac"); if (requestedIpv6 != null) { requestedIpv6 = NetUtils.standardizeIp6Address(requestedIpv6); } - IpAddresses addrs = new IpAddresses(requestedIp, requestedIpv6); + if (requestedMac != null) { + if(!NetUtils.isValidMac(requestedMac)) { + throw new InvalidParameterValueException("Mac address is not valid: " + requestedMac); + } else if(!NetUtils.isUnicastMac(requestedMac)) { + throw new InvalidParameterValueException("Mac address is not unicast: " + requestedMac); + } + requestedMac = NetUtils.standardizeMacAddress(requestedMac); + } + IpAddresses addrs = new IpAddresses(requestedIp, requestedIpv6, requestedMac); ipToNetworkMap.put(networkId, addrs); } } @@ -355,6 +367,19 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG return NetUtils.standardizeIp6Address(ip6Address); } + + public String getMacAddress() { + if (macAddress == null) { + return null; + } + if(!NetUtils.isValidMac(macAddress)) { + throw new InvalidParameterValueException("Mac address is not valid: " + macAddress); + } else if(!NetUtils.isUnicastMac(macAddress)) { + throw new InvalidParameterValueException("Mac address is not unicast: " + macAddress); + } + return NetUtils.standardizeMacAddress(macAddress); + } + public List getAffinityGroupIdList() { if (affinityGroupNameList != null && affinityGroupIdList != null) { throw new InvalidParameterValueException("affinitygroupids parameter is mutually exclusive with affinitygroupnames parameter"); diff --git a/engine/schema/src/com/cloud/vm/dao/NicDao.java b/engine/schema/src/com/cloud/vm/dao/NicDao.java index 797f002381b..0d86964974a 100644 --- a/engine/schema/src/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/com/cloud/vm/dao/NicDao.java @@ -44,6 +44,8 @@ public interface NicDao extends GenericDao { NicVO findByIp4AddressAndNetworkId(String ip4Address, long networkId); + NicVO findByNetworkIdAndMacAddress(long networkId, String mac); + NicVO findDefaultNicForVM(long instanceId); /** diff --git a/engine/schema/src/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/com/cloud/vm/dao/NicDaoImpl.java index daf773afbf7..f953a4c1f93 100644 --- a/engine/schema/src/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/com/cloud/vm/dao/NicDaoImpl.java @@ -68,6 +68,7 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { AllFieldsSearch.and("nicid", AllFieldsSearch.entity().getId(), Op.EQ); AllFieldsSearch.and("strategy", AllFieldsSearch.entity().getReservationStrategy(), Op.EQ); AllFieldsSearch.and("reserverName",AllFieldsSearch.entity().getReserver(),Op.EQ); + AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.EQ); AllFieldsSearch.done(); IpSearch = createSearchBuilder(String.class); @@ -198,6 +199,14 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { return findOneBy(sc); } + @Override + public NicVO findByNetworkIdAndMacAddress(long networkId, String mac) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("network", networkId); + sc.setParameters("macAddress", mac); + return findOneBy(sc); + } + @Override public NicVO findDefaultNicForVM(long instanceId) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/server/src/com/cloud/network/IpAddressManagerImpl.java b/server/src/com/cloud/network/IpAddressManagerImpl.java index f3584d1b650..9056ed5cdfc 100644 --- a/server/src/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/com/cloud/network/IpAddressManagerImpl.java @@ -1988,7 +1988,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage nic.setBroadcastUri(BroadcastDomainType.Vlan.toUri(ip.getVlanTag())); nic.setFormat(AddressFormat.Ip4); nic.setReservationId(String.valueOf(ip.getVlanTag())); - nic.setMacAddress(ip.getMacAddress()); + if(nic.getMacAddress() == null) { + nic.setMacAddress(ip.getMacAddress()); + } } nic.setIPv4Dns1(dc.getDns1()); nic.setIPv4Dns2(dc.getDns2()); @@ -2010,7 +2012,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage nic.setBroadcastUri(BroadcastDomainType.Vlan.toUri(vlan.getVlanTag())); nic.setFormat(AddressFormat.Ip6); nic.setReservationId(String.valueOf(vlan.getVlanTag())); - nic.setMacAddress(ip.getMacAddress()); + if(nic.getMacAddress() == null) { + nic.setMacAddress(ip.getMacAddress()); + } } } nic.setIPv6Dns1(dc.getIp6Dns1()); @@ -2057,8 +2061,9 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage nic.setBroadcastUri(network.getBroadcastUri()); nic.setFormat(AddressFormat.Ip4); - - nic.setMacAddress(_networkModel.getNextAvailableMacAddressInNetwork(network.getId())); + if(nic.getMacAddress() == null) { + nic.setMacAddress(_networkModel.getNextAvailableMacAddressInNetwork(network.getId())); + } } nic.setIPv4Dns1(dc.getDns1()); nic.setIPv4Dns2(dc.getDns2()); diff --git a/server/src/com/cloud/network/NetworkModelImpl.java b/server/src/com/cloud/network/NetworkModelImpl.java index 7caa328d444..2efec9a0999 100644 --- a/server/src/com/cloud/network/NetworkModelImpl.java +++ b/server/src/com/cloud/network/NetworkModelImpl.java @@ -62,6 +62,7 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.IpAddress.State; import com.cloud.network.Network.Capability; import com.cloud.network.Network.GuestType; +import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.Networks.TrafficType; @@ -2169,7 +2170,10 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi } @Override - public void checkRequestedIpAddresses(long networkId, String ip4, String ip6) throws InvalidParameterValueException { + public void checkRequestedIpAddresses(long networkId, IpAddresses ips) throws InvalidParameterValueException { + String ip4 = ips.getIp4Address(); + String ip6 = ips.getIp6Address(); + String mac = ips.getMacAddress(); if (ip4 != null) { if (!NetUtils.isValidIp(ip4)) { throw new InvalidParameterValueException("Invalid specified IPv4 address " + ip4); @@ -2198,6 +2202,15 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi throw new InvalidParameterValueException("Requested IPv6 is not in the predefined range!"); } } + if (mac != null) { + if(!NetUtils.isValidMac(mac)) { + throw new InvalidParameterValueException("Invalid specified MAC address " + mac); + } + if (_nicDao.findByNetworkIdAndMacAddress(networkId, mac) != null) { + throw new InvalidParameterValueException("The requested Mac address is already taken! " + mac); + } + + } } @Override diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index 3f8a1339740..952b850209f 100755 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -1136,6 +1136,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Long vmId = cmd.getVmId(); Long networkId = cmd.getNetworkId(); String ipAddress = cmd.getIpAddress(); + String macAddress = cmd.getMacAddress(); Account caller = CallContext.current().getCallingAccount(); UserVmVO vmInstance = _vmDao.findById(vmId); @@ -1166,12 +1167,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new CloudRuntimeException("A NIC already exists for VM:" + vmInstance.getInstanceName() + " in network: " + network.getUuid()); } - NicProfile profile = new NicProfile(null, null); + if(_nicDao.findByNetworkIdAndMacAddress(networkId, macAddress) != null) { + throw new CloudRuntimeException("A NIC with this MAC address exists for network: " + network.getUuid()); + } + + NicProfile profile = new NicProfile(ipAddress, null, macAddress); if (ipAddress != null) { if (!(NetUtils.isValidIp(ipAddress) || NetUtils.isValidIpv6(ipAddress))) { throw new InvalidParameterValueException("Invalid format for IP address parameter: " + ipAddress); } - profile = new NicProfile(ipAddress, null); } // Perform permission check on VM @@ -3363,17 +3367,19 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (requestedIpPair == null) { requestedIpPair = new IpAddresses(null, null); } else { - _networkModel.checkRequestedIpAddresses(network.getId(), requestedIpPair.getIp4Address(), requestedIpPair.getIp6Address()); + _networkModel.checkRequestedIpAddresses(network.getId(), requestedIpPair); } - NicProfile profile = new NicProfile(requestedIpPair.getIp4Address(), requestedIpPair.getIp6Address()); + NicProfile profile = new NicProfile(requestedIpPair.getIp4Address(), requestedIpPair.getIp6Address(), requestedIpPair.getMacAddress()); if (defaultNetworkNumber == 0) { defaultNetworkNumber++; // if user requested specific ip for default network, add it if (defaultIps.getIp4Address() != null || defaultIps.getIp6Address() != null) { - _networkModel.checkRequestedIpAddresses(network.getId(), defaultIps.getIp4Address(), defaultIps.getIp6Address()); + _networkModel.checkRequestedIpAddresses(network.getId(), defaultIps); profile = new NicProfile(defaultIps.getIp4Address(), defaultIps.getIp6Address()); + } else if (defaultIps.getMacAddress() != null) { + profile = new NicProfile(null, null, defaultIps.getMacAddress()); } profile.setDefaultNic(true); @@ -4625,10 +4631,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir String ipAddress = cmd.getIpAddress(); String ip6Address = cmd.getIp6Address(); + String macAddress = cmd.getMacAddress(); String name = cmd.getName(); String displayName = cmd.getDisplayName(); UserVm vm = null; - IpAddresses addrs = new IpAddresses(ipAddress, ip6Address); + IpAddresses addrs = new IpAddresses(ipAddress, ip6Address, macAddress); Long size = cmd.getSize(); String group = cmd.getGroup(); String userData = cmd.getUserData(); diff --git a/server/test/com/cloud/network/MockNetworkModelImpl.java b/server/test/com/cloud/network/MockNetworkModelImpl.java index 4b4aebd1de0..3c3fd7097cd 100644 --- a/server/test/com/cloud/network/MockNetworkModelImpl.java +++ b/server/test/com/cloud/network/MockNetworkModelImpl.java @@ -30,6 +30,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network.Capability; import com.cloud.network.Network.GuestType; +import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.Networks.IsolationType; @@ -830,7 +831,7 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel { } @Override - public void checkRequestedIpAddresses(long networkId, String ip4, String ip6) throws InvalidParameterValueException { + public void checkRequestedIpAddresses(long networkId, IpAddresses ips) throws InvalidParameterValueException { // TODO Auto-generated method stub } diff --git a/server/test/com/cloud/vpc/MockNetworkModelImpl.java b/server/test/com/cloud/vpc/MockNetworkModelImpl.java index 6cd1ea1beaa..50d9b0f6425 100644 --- a/server/test/com/cloud/vpc/MockNetworkModelImpl.java +++ b/server/test/com/cloud/vpc/MockNetworkModelImpl.java @@ -33,6 +33,7 @@ import com.cloud.network.IpAddress; import com.cloud.network.Network; import com.cloud.network.Network.Capability; import com.cloud.network.Network.GuestType; +import com.cloud.network.Network.IpAddresses; import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.NetworkModel; @@ -845,7 +846,7 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel { } @Override - public void checkRequestedIpAddresses(long networkId, String ip4, String ip6) throws InvalidParameterValueException { + public void checkRequestedIpAddresses(long networkId, IpAddresses ips) throws InvalidParameterValueException { // TODO Auto-generated method stub } diff --git a/test/integration/smoke/test_nic.py b/test/integration/smoke/test_nic.py index 7067074c78b..9dc385c5432 100644 --- a/test/integration/smoke/test_nic.py +++ b/test/integration/smoke/test_nic.py @@ -30,6 +30,7 @@ from nose.plugins.attrib import attr import signal import sys +import logging import time @@ -37,6 +38,11 @@ class TestNic(cloudstackTestCase): def setUp(self): self.cleanup = [] + self.logger = logging.getLogger('TestNIC') + self.stream_handler = logging.StreamHandler() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.stream_handler) + def signal_handler(signal, frame): self.tearDown() @@ -278,6 +284,38 @@ class TestNic(cloudstackTestCase): return + def test_02_nic_with_mac(self): + """Test to add and update added nic to a virtual machine with specific mac""" + + hypervisorIsVmware = False + isVmwareToolInstalled = False + if self.hypervisor.lower() == "vmware": + hypervisorIsVmware = True + + self.virtual_machine2 = VirtualMachine.create( + self.apiclient, + self.services["small"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + networkids=[self.test_network.id], + macaddress="aa:bb:cc:dd:ee:ff", + mode=self.zone.networktype if hypervisorIsVmware else "default" + ) + self.cleanup.insert(0, self.virtual_machine2) + self.assertEqual(self.virtual_machine2.nic[0].macaddress, "aa:bb:cc:dd:ee:ff", "Mac address not honored") + vmdata = self.virtual_machine2.add_nic( + self.apiclient, + self.test_network2.id, + macaddress="ee:ee:dd:cc:bb:aa") + found = False + for n in vmdata.nic: + if n.macaddress == "ee:ee:dd:cc:bb:aa": + found = True + break + + self.assertTrue(found, "Nic not successfully added with specified mac address") + def tearDown(self): try: for obj in self.cleanup: diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index f96cd8b26d1..ffe1e01f922 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -449,7 +449,7 @@ class VirtualMachine: hostid=None, keypair=None, ipaddress=None, mode='default', method='GET', hypervisor=None, customcpunumber=None, customcpuspeed=None, custommemory=None, rootdisksize=None, - rootdiskcontroller=None): + rootdiskcontroller=None, macaddress=None): """Create the instance""" cmd = deployVirtualMachine.deployVirtualMachineCmd() @@ -563,6 +563,11 @@ class VirtualMachine: if mode.lower() == 'basic': cls.ssh_access_group(apiclient, cmd) + if macaddress: + cmd.macaddress = macaddress + elif macaddress in services: + cmd.macaddress = services["macaddress"] + virtual_machine = apiclient.deployVirtualMachine(cmd, method=method) virtual_machine.ssh_ip = virtual_machine.nic[0].ipaddress @@ -775,7 +780,7 @@ class VirtualMachine: cmd.id = volume.id return apiclient.detachVolume(cmd) - def add_nic(self, apiclient, networkId, ipaddress=None): + def add_nic(self, apiclient, networkId, ipaddress=None, macaddress=None): """Add a NIC to a VM""" cmd = addNicToVirtualMachine.addNicToVirtualMachineCmd() cmd.virtualmachineid = self.id @@ -784,6 +789,9 @@ class VirtualMachine: if ipaddress: cmd.ipaddress = ipaddress + if macaddress: + cmd.macaddress = macaddress + return apiclient.addNicToVirtualMachine(cmd) def remove_nic(self, apiclient, nicId): diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index 4972070ea66..c28739a91bd 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -44,6 +44,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.apache.commons.net.util.SubnetUtils; import org.apache.commons.validator.routines.InetAddressValidator; +import org.apache.commons.validator.routines.RegexValidator; import org.apache.log4j.Logger; import com.cloud.utils.IteratorUtil; @@ -1179,6 +1180,23 @@ public class NetUtils { return false; } + public static boolean isValidMac(final String macAddr) { + RegexValidator mv = new RegexValidator("^(?:[0-9a-f]{1,2}([-:\\.]))(?:[0-9a-f]{1,2}\\1){4}[0-9a-f]{1,2}$", false); + return mv.isValid(macAddr); + } + + public static boolean isUnicastMac(final String macAddr) { + String std = standardizeMacAddress(macAddr); + if(std == null) { + return false; + } + long stdl = mac2Long(std); + // libvirt refuses to attach a mac address that is multicast, as defined + // by the least significant bit of the first octet of the mac. + long mask = 0x1l << 40l; + return ((stdl & mask) == mask) ? false : true; + } + public static boolean verifyInstanceName(final String instanceName) { //instance name for cloudstack vms shouldn't contain - and spaces if (instanceName.contains("-") || instanceName.contains(" ") || instanceName.contains("+")) { @@ -1456,6 +1474,15 @@ public class NetUtils { } } + public static String standardizeMacAddress(final String macAddr) { + if (!isValidMac(macAddr)) { + return null; + } + String norm = macAddr.replace('.', ':'); + norm = norm.replace('-', ':'); + return long2Mac(mac2Long(norm)); + } + public static String standardizeIp6Cidr(final String ip6Cidr){ try { return IPv6Network.fromString(ip6Cidr).toString(); diff --git a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java index fb769dedd13..eaa9c29088e 100644 --- a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java @@ -178,6 +178,24 @@ public class NetUtilsTest { assertFalse(NetUtils.isValidIp6Cidr("1234:5678::1")); } + @Test + public void testIsValidMacAddr() { + assertTrue(NetUtils.isValidMac("ee:12:34:5:32:ff")); + assertTrue(NetUtils.isValidMac("ee.12.34.5.32.ff")); + assertTrue(NetUtils.isValidMac("ee-12-34-5-32-ff")); + assertFalse(NetUtils.isValidMac("aa.12:34:5:32:ff")); + assertFalse(NetUtils.isValidMac("gg.gg:gg:gg:gg:gg")); + } + + @Test + public void testIsUnicastMac() { + + assertTrue(NetUtils.isUnicastMac("ee:12:34:5:32:ff")); + assertFalse(NetUtils.isUnicastMac("ff:12:34:5:32:ff")); + assertFalse(NetUtils.isUnicastMac("01:12:34:5:32:ff")); + assertTrue(NetUtils.isUnicastMac("00:ff:ff:ff:ff:ff")); + } + @Test public void testIsValidIpv6() { assertTrue(NetUtils.isValidIpv6("fc00::1"));