github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/cmd/hyper-control/netbootHost.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net"
    10  
    11  	imageclient "github.com/Cloud-Foundations/Dominator/imageserver/client"
    12  	"github.com/Cloud-Foundations/Dominator/lib/errors"
    13  	libjson "github.com/Cloud-Foundations/Dominator/lib/json"
    14  	"github.com/Cloud-Foundations/Dominator/lib/log"
    15  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    16  	fm_proto "github.com/Cloud-Foundations/Dominator/proto/fleetmanager"
    17  	hyper_proto "github.com/Cloud-Foundations/Dominator/proto/hypervisor"
    18  	installer_proto "github.com/Cloud-Foundations/Dominator/proto/installer"
    19  )
    20  
    21  type hostAddressType struct {
    22  	address  hyper_proto.Address
    23  	hostname string
    24  }
    25  
    26  type leaseType struct {
    27  	address hostAddressType
    28  	subnet  *hyper_proto.Subnet
    29  }
    30  
    31  func netbootHostSubcommand(args []string, logger log.DebugLogger) error {
    32  	err := netbootHost(args[0], logger)
    33  	if err != nil {
    34  		return fmt.Errorf("Error netbooting host: %s", err)
    35  	}
    36  	return nil
    37  }
    38  
    39  func findMatchingSubnet(subnets []*hyper_proto.Subnet,
    40  	ipAddr net.IP) *hyper_proto.Subnet {
    41  	for _, subnet := range subnets {
    42  		subnetMask := net.IPMask(subnet.IpMask)
    43  		subnetAddr := subnet.IpGateway.Mask(subnetMask)
    44  		if ipAddr.Mask(subnetMask).Equal(subnetAddr) {
    45  			return subnet
    46  		}
    47  	}
    48  	return nil
    49  }
    50  
    51  func getHostAddress(networkEntries []fm_proto.NetworkEntry) []hostAddressType {
    52  	hostAddresses := make([]hostAddressType, 0, len(networkEntries))
    53  	for _, networkEntry := range networkEntries {
    54  		if len(networkEntry.HostIpAddress) > 0 &&
    55  			len(networkEntry.HostMacAddress) > 0 {
    56  			hostAddresses = append(hostAddresses, hostAddressType{
    57  				address: hyper_proto.Address{
    58  					IpAddress:  networkEntry.HostIpAddress,
    59  					MacAddress: networkEntry.HostMacAddress.String(),
    60  				},
    61  				hostname: networkEntry.Hostname,
    62  			})
    63  		}
    64  	}
    65  	return hostAddresses
    66  }
    67  
    68  func getInstallConfig(fmCR *srpc.ClientResource, imageClient *srpc.Client,
    69  	hostname string, addRandomData bool,
    70  	logger log.DebugLogger) (*fm_proto.GetMachineInfoResponse, []leaseType,
    71  	map[string][]byte, error) {
    72  	info, err := getInfoForMachine(fmCR, hostname)
    73  	if err != nil {
    74  		return nil, nil, nil, err
    75  	}
    76  	imageName := info.Machine.Tags["RequiredImage"]
    77  	subnets := make([]*hyper_proto.Subnet, 0, len(info.Subnets))
    78  	for _, subnet := range info.Subnets {
    79  		if subnet.VlanId == 0 {
    80  			subnets = append(subnets, subnet)
    81  		}
    82  	}
    83  	if len(subnets) < 1 {
    84  		return nil, nil, nil, errors.New("no non-VLAN subnets known")
    85  	}
    86  	networkEntries := getNetworkEntries(info)
    87  	hostAddresses := getHostAddress(networkEntries)
    88  	if len(hostAddresses) < 1 {
    89  		return nil, nil, nil,
    90  			errors.New("no IP and MAC addresses known for host")
    91  	}
    92  	leases := make([]leaseType, 0, len(hostAddresses))
    93  	for _, address := range hostAddresses {
    94  		subnet := findMatchingSubnet(subnets, address.address.IpAddress)
    95  		if subnet != nil {
    96  			leases = append(leases, leaseType{address: address, subnet: subnet})
    97  		}
    98  	}
    99  	if len(leases) < 1 {
   100  		return nil, nil, nil,
   101  			errors.New("no IP and MAC addresses matching a subnet")
   102  	}
   103  	if imageName != "" {
   104  		img, err := imageclient.GetImage(imageClient, imageName)
   105  		if err != nil {
   106  			return nil, nil, nil, err
   107  		}
   108  		if img == nil {
   109  			logger.Printf("warning: image: %s does not exist", imageName)
   110  		} else if len(img.FileSystem.InodeTable) < 1000 {
   111  			return nil, nil, nil, fmt.Errorf(
   112  				"only %d inodes, this is likely not bootable",
   113  				len(img.FileSystem.InodeTable))
   114  		}
   115  	}
   116  	configFiles, err := makeConfigFiles(info, imageName, networkEntries,
   117  		addRandomData)
   118  	if err != nil {
   119  		return nil, nil, nil, err
   120  	}
   121  	return &info, leases, configFiles, nil
   122  }
   123  
   124  func getNetworkEntries(
   125  	info fm_proto.GetMachineInfoResponse) []fm_proto.NetworkEntry {
   126  	networkEntries := make([]fm_proto.NetworkEntry, 1,
   127  		len(info.Machine.SecondaryNetworkEntries)+1)
   128  	networkEntries[0] = info.Machine.NetworkEntry
   129  	for _, networkEntry := range info.Machine.SecondaryNetworkEntries {
   130  		networkEntries = append(networkEntries, networkEntry)
   131  	}
   132  	return networkEntries
   133  }
   134  
   135  func getStorageLayout() (installer_proto.StorageLayout, error) {
   136  	if *storageLayoutFilename == "" {
   137  		return makeDefaultStorageLayout(), nil
   138  	}
   139  	var val installer_proto.StorageLayout
   140  	if err := libjson.ReadFromFile(*storageLayoutFilename, &val); err != nil {
   141  		return installer_proto.StorageLayout{}, err
   142  	}
   143  	return val, nil
   144  }
   145  
   146  func makeConfigFiles(info fm_proto.GetMachineInfoResponse, imageName string,
   147  	networkEntries []fm_proto.NetworkEntry,
   148  	addRandomData bool) (map[string][]byte, error) {
   149  	filesMap := make(map[string][]byte, len(netbootFiles)+1)
   150  	for tftpFilename, localFilename := range netbootFiles {
   151  		if data, err := ioutil.ReadFile(localFilename); err != nil {
   152  			return nil, err
   153  		} else {
   154  			filesMap[tftpFilename] = data
   155  		}
   156  	}
   157  	if data, err := json.MarshalIndent(info, "", "    "); err != nil {
   158  		return nil, err
   159  	} else {
   160  		filesMap["config.json"] = append(data, '\n')
   161  	}
   162  	if *targetImageName != "" {
   163  		filesMap["imagename"] = []byte(*targetImageName + "\n")
   164  	} else if imageName != "" {
   165  		filesMap["imagename"] = []byte(imageName + "\n")
   166  	}
   167  	buffer := new(bytes.Buffer)
   168  	fmt.Fprintf(buffer, "%s:%d\n", *imageServerHostname, *imageServerPortNum)
   169  	filesMap["imageserver"] = buffer.Bytes()
   170  	var primarySubnet *hyper_proto.Subnet
   171  	for _, networkEntry := range networkEntries {
   172  		subnet := findMatchingSubnet(info.Subnets, networkEntry.HostIpAddress)
   173  		if subnet == nil {
   174  			continue
   175  		}
   176  		primarySubnet = subnet
   177  		break
   178  	}
   179  	if primarySubnet == nil {
   180  		return nil, errors.New("no primary subnet found")
   181  	}
   182  	if addRandomData && *randomSeedBytes > 0 {
   183  		randomData := make([]byte, *randomSeedBytes)
   184  		if _, err := rand.Read(randomData); err != nil {
   185  			return nil, err
   186  		}
   187  		filesMap["random-seed"] = randomData
   188  	}
   189  	buffer = new(bytes.Buffer)
   190  	if primarySubnet.DomainName != "" {
   191  		fmt.Fprintf(buffer, "domain %s\n", primarySubnet.DomainName)
   192  		fmt.Fprintf(buffer, "search %s\n", primarySubnet.DomainName)
   193  		fmt.Fprintln(buffer)
   194  	}
   195  	for _, nameserver := range primarySubnet.DomainNameServers {
   196  		fmt.Fprintf(buffer, "nameserver %s\n", nameserver)
   197  	}
   198  	filesMap["resolv.conf"] = buffer.Bytes()
   199  	if layout, err := getStorageLayout(); err != nil {
   200  		return nil, err
   201  	} else {
   202  		if data, err := json.MarshalIndent(layout, "", "    "); err != nil {
   203  			return nil, err
   204  		} else {
   205  			filesMap["storage-layout.json"] = append(data, '\n')
   206  		}
   207  	}
   208  	return filesMap, nil
   209  }
   210  
   211  func makeDefaultStorageLayout() installer_proto.StorageLayout {
   212  	return installer_proto.StorageLayout{
   213  		BootDriveLayout: []installer_proto.Partition{
   214  			{
   215  				MountPoint:       "/",
   216  				MinimumFreeBytes: 2 << 30,
   217  			},
   218  			{
   219  				MountPoint:       "/home",
   220  				MinimumFreeBytes: 1 << 30,
   221  			},
   222  			{
   223  				MountPoint:       "/var/log",
   224  				MinimumFreeBytes: 512 << 20,
   225  			},
   226  		},
   227  		ExtraMountPointsBasename: "/data/",
   228  		UseKexec:                 *useKexec,
   229  	}
   230  }
   231  
   232  func netbootHost(hostname string, logger log.DebugLogger) error {
   233  	fmCR := srpc.NewClientResource("tcp",
   234  		fmt.Sprintf("%s:%d", *fleetManagerHostname, *fleetManagerPortNum))
   235  	defer fmCR.ScheduleClose()
   236  	imageClient, err := srpc.DialHTTP("tcp", fmt.Sprintf("%s:%d",
   237  		*imageServerHostname, *imageServerPortNum), 0)
   238  	if err != nil {
   239  		return fmt.Errorf("%s: %s", *imageServerHostname, err)
   240  	}
   241  	defer imageClient.Close()
   242  	info, leases, configFiles, err := getInstallConfig(fmCR, imageClient,
   243  		hostname, true, logger)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	var hypervisorAddresses []string
   248  	if *hypervisorHostname != "" {
   249  		hypervisorAddresses = append(hypervisorAddresses,
   250  			fmt.Sprintf("%s:%d", *hypervisorHostname, *hypervisorPortNum))
   251  	} else {
   252  		hypervisorAddresses, err = listConnectedHypervisorsInLocation(fmCR,
   253  			info.Location)
   254  		if err != nil {
   255  			return err
   256  		}
   257  	}
   258  	if len(hypervisorAddresses) < 1 {
   259  		return errors.New("no nearby Hypervisors available")
   260  	}
   261  	logger.Debugf(0, "Selected %s as boot server on subnet: %s\n",
   262  		hypervisorAddresses[0], leases[0].subnet.Id)
   263  	hyperCR := srpc.NewClientResource("tcp", hypervisorAddresses[0])
   264  	defer hyperCR.ScheduleClose()
   265  	request := hyper_proto.NetbootMachineRequest{
   266  		Address:                      leases[0].address.address,
   267  		Files:                        configFiles,
   268  		FilesExpiration:              *netbootFilesTimeout,
   269  		Hostname:                     hostname,
   270  		NumAcknowledgementsToWaitFor: *numAcknowledgementsToWaitFor,
   271  		OfferExpiration:              *offerTimeout,
   272  		WaitTimeout:                  *netbootTimeout,
   273  	}
   274  	var reply hyper_proto.NetbootMachineResponse
   275  	client, err := hyperCR.GetHTTP(nil, 0)
   276  	if err != nil {
   277  		return err
   278  	}
   279  	defer client.Put()
   280  	err = client.RequestReply("Hypervisor.NetbootMachine", request, &reply)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	return errors.New(reply.Error)
   285  }