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.
This commit is contained in:
Nathan Johnson 2017-09-16 01:46:42 -05:00 committed by Rohit Yadav
parent 2ccea134ae
commit 2bad9a6c11
17 changed files with 210 additions and 18 deletions

View File

@ -277,12 +277,26 @@ public interface Network extends ControlledEntity, StateObject<Network.State>, 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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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";

View File

@ -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///////////////////
/////////////////////////////////////////////////////

View File

@ -147,7 +147,7 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG
private List<String> 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<Long> getAffinityGroupIdList() {
if (affinityGroupNameList != null && affinityGroupIdList != null) {
throw new InvalidParameterValueException("affinitygroupids parameter is mutually exclusive with affinitygroupnames parameter");

View File

@ -44,6 +44,8 @@ public interface NicDao extends GenericDao<NicVO, Long> {
NicVO findByIp4AddressAndNetworkId(String ip4Address, long networkId);
NicVO findByNetworkIdAndMacAddress(long networkId, String mac);
NicVO findDefaultNicForVM(long instanceId);
/**

View File

@ -68,6 +68,7 @@ public class NicDaoImpl extends GenericDaoBase<NicVO, Long> 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<NicVO, Long> implements NicDao {
return findOneBy(sc);
}
@Override
public NicVO findByNetworkIdAndMacAddress(long networkId, String mac) {
SearchCriteria<NicVO> sc = AllFieldsSearch.create();
sc.setParameters("network", networkId);
sc.setParameters("macAddress", mac);
return findOneBy(sc);
}
@Override
public NicVO findDefaultNicForVM(long instanceId) {
SearchCriteria<NicVO> sc = AllFieldsSearch.create();

View File

@ -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());

View File

@ -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

View File

@ -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();

View File

@ -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
}

View File

@ -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
}

View File

@ -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:

View File

@ -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):

View File

@ -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();

View File

@ -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"));