diff --git a/client/workloads.go b/client/workloads.go index 339d1b43..b8328342 100644 --- a/client/workloads.go +++ b/client/workloads.go @@ -137,6 +137,12 @@ func (wl *intermediateWL) Workload() (result workloads.ReservationWorkload, err return result, err } result.Content = o + case workloads.WorkloadTypeQemu: + var o workloads.Qemu + if err := json.Unmarshal(wl.Content, &o); err != nil { + return result, err + } + result.Content = o default: return result, fmt.Errorf("unknown workload type") } diff --git a/cmds/tfuser/cmds_provision.go b/cmds/tfuser/cmds_provision.go index 9c6de686..fdf65132 100644 --- a/cmds/tfuser/cmds_provision.go +++ b/cmds/tfuser/cmds_provision.go @@ -30,6 +30,7 @@ func cmdsProvision(c *cli.Context) error { zdbs = c.StringSlice("zdb") kubes = c.StringSlice("kube") networks = c.StringSlice("network") + qemus = c.StringSlice("qemu") dryRun = c.Bool("dry-run") err error ) @@ -97,6 +98,21 @@ func cmdsProvision(c *cli.Context) error { reservationBuilder.AddK8s(*k8sBuilder) } + for _, qemu := range qemus { + f, err := os.Open(qemu) + if err != nil { + return errors.Wrap(err, "failed to open qemu") + } + + qemuBuilder, err := builders.LoadQemuBuilder(f) + if err != nil { + return errors.Wrap(err, "failed to load the qemu builder") + } + qemuBuilder.WorkloadId = workloadID + workloadID = +1 + reservationBuilder.AddQemu(*qemuBuilder) + } + for _, network := range networks { f, err := os.Open(network) if err != nil { diff --git a/cmds/tfuser/cmds_qemu.go b/cmds/tfuser/cmds_qemu.go new file mode 100644 index 00000000..1598a64b --- /dev/null +++ b/cmds/tfuser/cmds_qemu.go @@ -0,0 +1,41 @@ +package main + +import ( + "net" + + "github.com/pkg/errors" + "github.com/threefoldtech/tfexplorer/models/generated/workloads" + "github.com/threefoldtech/tfexplorer/provision/builders" + "github.com/urfave/cli" +) + +func generateQemu(c *cli.Context) error { + var ( + nodeID = c.String("node") + netID = c.String("network-id") + ipString = c.String("ip") + image = c.String("image") + ) + + ip := net.ParseIP(ipString) + if ip.To4() == nil { + return errors.New("bad IP for vm") + } + + if netID == "" { + return errors.New("vm requires a network to run in") + } + + /* if image == "" { + return errors.New("vm requires a image to boot from") + } */ + + cap := workloads.QemuCapacity{ + CPU: c.Uint("cpu"), + Memory: c.Uint64("memory"), + HDDSize: c.Uint64("hddsize"), + } + + qemu := builders.NewQemuBuilder(nodeID, netID, ip, image, cap) + return writeWorkload(c.GlobalString("schema"), qemu.Build()) +} diff --git a/cmds/tfuser/main.go b/cmds/tfuser/main.go index e0d00a1b..c6047254 100644 --- a/cmds/tfuser/main.go +++ b/cmds/tfuser/main.go @@ -437,6 +437,46 @@ func main() { }, Action: generateKubernetes, }, + { + Name: "qemu", + Usage: "Provision a virtual machine using Qemu as hypervisor", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "node", + Usage: "node id for the generated workload", + Required: true, + }, + cli.StringFlag{ + Name: "flist", + Usage: "URL to the flist of the image", + Required: true, + }, + cli.StringFlag{ + Name: "network-id", + Usage: "ID of the network resource in which the vm will be created", + }, + cli.StringFlag{ + Name: "ip", + Usage: "Ip address of the virtual machine in the network resource", + }, + cli.UintFlag{ + Name: "cpu", + Usage: "limit the amount of CPU allocated to the virtual machine", + Required: true, + }, + cli.Uint64Flag{ + Name: "memory", + Usage: "limit the amount of memory a virutal machine can allocate", + Required: true, + }, + cli.Uint64Flag{ + Name: "HDD", + Usage: "limit the amount of HDD (in GB) a virtual machine can allocate", + Required: true, + }, + }, + Action: generateQemu, + }, }, }, { @@ -476,6 +516,10 @@ func main() { Name: "network", Usage: "add a network to provision", }, + cli.StringSliceFlag{ + Name: "qemu", + Usage: "add a qemu vm to provision", + }, }, Action: cmdsProvision, }, diff --git a/models/generated/directory/directory.go b/models/generated/directory/directory.go index 865bea16..d3acf089 100644 --- a/models/generated/directory/directory.go +++ b/models/generated/directory/directory.go @@ -208,6 +208,7 @@ type WorkloadAmount struct { ZDBNamespace uint16 `bson:"zdb_namespace" json:"zdb_namespace"` Container uint16 `bson:"container" json:"container"` K8sVM uint16 `bson:"k8s_vm" json:"k8s_vm"` + QemuVM uint16 `bson:"qemu_vm" json:"qemu_vm"` Proxy uint16 `bson:"proxy" json:"proxy"` ReverseProxy uint16 `bson:"reverse_proxy" json:"reverse_proxy"` Subdomain uint16 `bson:"subdomain" json:"subdomain"` diff --git a/models/generated/workloads/qemu.go b/models/generated/workloads/qemu.go new file mode 100644 index 00000000..de5da2e5 --- /dev/null +++ b/models/generated/workloads/qemu.go @@ -0,0 +1,26 @@ +package workloads + +import "net" + +type Qemu struct { + WorkloadId int64 `bson:"workload_id" json:"workload_id"` + NodeId string `bson:"node_id" json:"node_id"` + NetworkId string `bson:"network_id" json:"network_id"` + Ipaddress net.IP `bson:"ipaddress" json:"ipaddress"` + Image string `bson:"image" json:"image"` + Capacity QemuCapacity `bson:"capacity" json:"capacity"` +} + +func (q Qemu) WorkloadID() int64 { + return q.WorkloadId +} + +// QemuCapacity is the amount of resource to allocate to the virtual machine +type QemuCapacity struct { + // Number of CPU + CPU uint `json:"cpu"` + // Memory in MiB + Memory uint64 `json:"memory"` + // HDD in GB + HDDSize uint64 `json:"hdd"` +} diff --git a/models/generated/workloads/reservation.go b/models/generated/workloads/reservation.go index 7dfb87cf..be471c5c 100644 --- a/models/generated/workloads/reservation.go +++ b/models/generated/workloads/reservation.go @@ -63,6 +63,7 @@ type ReservationData struct { Zdbs []ZDB `bson:"zdbs" json:"zdbs"` Networks []Network `bson:"networks" json:"networks"` Kubernetes []K8S `bson:"kubernetes" json:"kubernetes"` + Qemus []Qemu `bson:"qemus" json:"qemus"` Proxies []GatewayProxy `bson:"proxies" json:"proxies"` ReserveProxy []GatewayReserveProxy `bson:"reserve_proxies" json:"reserve_proxies"` Subdomains []GatewaySubdomain `bson:"subdomains" json:"subdomains"` @@ -138,6 +139,7 @@ const ( WorkloadTypeSubDomain WorkloadTypeDomainDelegate WorkloadTypeGateway4To6 + WorkloadTypeQemu ) // WorkloadTypes is a map of all the supported workload type @@ -152,6 +154,7 @@ var WorkloadTypes = map[WorkloadTypeEnum]string{ WorkloadTypeSubDomain: "subdomain", WorkloadTypeDomainDelegate: "domain-delegate", WorkloadTypeGateway4To6: "gateway4to6", + WorkloadTypeQemu: "qemu", } func (e WorkloadTypeEnum) String() string { diff --git a/models/schema/workloads/tfgrid_workloads_reservation_1.toml b/models/schema/workloads/tfgrid_workloads_reservation_1.toml index 079d995f..e207d078 100644 --- a/models/schema/workloads/tfgrid_workloads_reservation_1.toml +++ b/models/schema/workloads/tfgrid_workloads_reservation_1.toml @@ -30,6 +30,7 @@ volumes = (LO) !tfgrid.workloads.reservation.volume.1 zdbs = (LO) !tfgrid.workloads.reservation.zdb.1 networks = (LO) !tfgrid.workloads.reservation.network.1 kubernetes = (LO) !tfgrid.workloads.reservation.k8s.1 +qemu = (LO) !tfgrid.workloads.reservation.qemu.1 proxies = (LO) !tfgrid.workloads.reservation.gateway.proxy.1 reserve_proxies = (LO) !tfgrid.workloads.reservation.gateway.reserve_proxy.1 subdomain = (LO) !tfgrid.workloads.reservation.gateway.subdomain.1 diff --git a/models/schema/workloads/tfgrid_workloads_reservation_qemu.toml b/models/schema/workloads/tfgrid_workloads_reservation_qemu.toml new file mode 100644 index 00000000..7d009b04 --- /dev/null +++ b/models/schema/workloads/tfgrid_workloads_reservation_qemu.toml @@ -0,0 +1,16 @@ +@url = tfgrid.workloads.reservation.qemu.1 +#unique id inside the reservation is an autoincrement +workload_id = (I) +#links to unique node on the TFGrid +node_id = (S) +network_id = (S) +ipaddress = (ipaddress) +image = (S) + +@url = tfgrid.workloads.reservation.qemu.capacity.1 +# Number of vCPU +cpu = (I) +# memory in MiB +memory = (I) +# HDD in GB +hdd = (I) diff --git a/pkg/escrow/escrow.go b/pkg/escrow/escrow.go index e4665698..d9e25113 100644 --- a/pkg/escrow/escrow.go +++ b/pkg/escrow/escrow.go @@ -34,11 +34,10 @@ func (e *Free) Run(ctx context.Context) error { // RegisterReservation implements the escrow interface func (e *Free) RegisterReservation(reservation workloads.Reservation, _ []string) (detail types.CustomerEscrowInformation, err error) { - if reservation.NextAction == workloads.NextActionPay { if err = workloadstypes.ReservationSetNextAction(context.Background(), e.db, reservation.ID, workloads.NextActionDeploy); err != nil { err = errors.Wrapf(err, "failed to change state of reservation %d to DEPLOY", reservation.ID) - return + return types.CustomerEscrowInformation{}, err } } diff --git a/pkg/workloads/reservation.go b/pkg/workloads/reservation.go index a3ef1e00..c293df9d 100644 --- a/pkg/workloads/reservation.go +++ b/pkg/workloads/reservation.go @@ -414,6 +414,12 @@ func (a *API) queued(ctx context.Context, db *mongo.Database, nodeID string, lim return nil, err } obj.Content = data + case generated.WorkloadTypeQemu: + var data generated.Qemu + if err := bson.Unmarshal(wl.Content, &data); err != nil { + return nil, err + } + obj.Content = data } workloads = append(workloads, types.Workload{ diff --git a/pkg/workloads/types/reservation.go b/pkg/workloads/types/reservation.go index 0cc0b032..712a47ff 100644 --- a/pkg/workloads/types/reservation.go +++ b/pkg/workloads/types/reservation.go @@ -100,7 +100,7 @@ func (f ReservationFilter) WithNodeID(id string) ReservationFilter { // we need to search ALL types for any reservation that has the node ID or := []bson.M{} - for _, typ := range []string{"containers", "volumes", "zdbs", "kubernetes", "proxies", "reserve_proxies", "subdomains", "domain_delegates", "gateway4to6"} { + for _, typ := range []string{"containers", "volumes", "zdbs", "kubernetes", "qemus", "proxies", "reserve_proxies", "subdomains", "domain_delegates", "gateway4to6"} { key := fmt.Sprintf("data_reservation.%s.node_id", typ) or = append(or, bson.M{key: id}) } @@ -190,7 +190,8 @@ func (r *Reservation) Validate() error { len(r.DataReservation.ReserveProxy) + len(r.DataReservation.Subdomains) + len(r.DataReservation.DomainDelegates) + - len(r.DataReservation.Gateway4To6s) + len(r.DataReservation.Gateway4To6s) + + len(r.DataReservation.Qemus) // all workloads are supposed to implement this interface type workloader interface{ WorkloadID() int64 } @@ -227,6 +228,9 @@ func (r *Reservation) Validate() error { for _, w := range r.DataReservation.Gateway4To6s { workloaders = append(workloaders, w) } + for _, w := range r.DataReservation.Qemus { + workloaders = append(workloaders, w) + } for _, w := range workloaders { if _, ok := ids[w.WorkloadID()]; ok { @@ -440,6 +444,18 @@ func (r *Reservation) Workloads(nodeID string) []Workload { wrkl.Content = wl workloads = append(workloads, wrkl) } + for _, wl := range data.Qemus { + if len(nodeID) > 0 && wl.NodeId != nodeID { + continue + } + wrkl := newWrkl( + fmt.Sprintf("%d-%d", r.ID, wl.WorkloadId), + generated.WorkloadTypeQemu, + wl.NodeId) + wrkl.Content = wl + workloads = append(workloads, wrkl) + + } for _, wl := range data.Networks { for _, nr := range wl.NetworkResources { @@ -506,6 +522,9 @@ func (r *Reservation) NodeIDs() []string { for _, w := range r.DataReservation.Proxies { ids[w.NodeId] = struct{}{} } + for _, w := range r.DataReservation.Qemus { + ids[w.NodeId] = struct{}{} + } nodeIDs := make([]string, 0, len(ids)) for nid := range ids { diff --git a/provision/builders/qemuBuilder.go b/provision/builders/qemuBuilder.go new file mode 100644 index 00000000..1f88ac61 --- /dev/null +++ b/provision/builders/qemuBuilder.go @@ -0,0 +1,83 @@ +package builders + +import ( + "encoding/json" + "io" + "net" + + "github.com/threefoldtech/tfexplorer/models/generated/workloads" +) + +// QemuBuilder is a struct that can build K8S's +type QemuBuilder struct { + workloads.Qemu +} + +// NewQemuBuilder creates a new Qemu builder +func NewQemuBuilder(nodeID string, networkID string, IP net.IP, image string, capacity workloads.QemuCapacity) *QemuBuilder { + return &QemuBuilder{ + Qemu: workloads.Qemu{ + NodeId: nodeID, + NetworkId: networkID, + Ipaddress: IP, + Image: image, + Capacity: capacity, + }, + } +} + +// LoadQemuBuilder loads a qemu builder based on a file path +func LoadQemuBuilder(reader io.Reader) (*QemuBuilder, error) { + qemu := workloads.Qemu{} + + err := json.NewDecoder(reader).Decode(&qemu) + if err != nil { + return &QemuBuilder{}, err + } + + return &QemuBuilder{Qemu: qemu}, nil +} + +// Save saves the Qemu builder to an IO.Writer +func (qemu *QemuBuilder) Save(writer io.Writer) error { + err := json.NewEncoder(writer).Encode(qemu.Qemu) + if err != nil { + return err + } + return err +} + +// Build returns the qemu +func (qemu *QemuBuilder) Build() workloads.Qemu { + return qemu.Qemu +} + +// WithNodeID sets the node ID to the Qemu +func (qemu *QemuBuilder) WithNodeID(nodeID string) *QemuBuilder { + qemu.Qemu.NodeId = nodeID + return qemu +} + +// WithIPAddress sets the ip address to the Qemu +func (qemu *QemuBuilder) WithIPAddress(ip net.IP) *QemuBuilder { + qemu.Qemu.Ipaddress = ip + return qemu +} + +// WithNetworkID sets the ip address to the Qemu +func (qemu *QemuBuilder) WithNetworkID(netID string) *QemuBuilder { + qemu.Qemu.NetworkId = netID + return qemu +} + +// WithImage sets the image id to the Qemu +func (qemu *QemuBuilder) WithImage(image string) *QemuBuilder { + qemu.Qemu.Image = image + return qemu +} + +// WithCapacity sets the capacity id to the Qemu +func (qemu *QemuBuilder) WithCapacity(capacity workloads.QemuCapacity) *QemuBuilder { + qemu.Qemu.Capacity = capacity + return qemu +} diff --git a/provision/builders/reservationBuilder.go b/provision/builders/reservationBuilder.go index b7e9b7a8..bbee2181 100644 --- a/provision/builders/reservationBuilder.go +++ b/provision/builders/reservationBuilder.go @@ -107,6 +107,12 @@ func (r *ReservationBuilder) AddK8s(k8s K8sBuilder) *ReservationBuilder { return r } +// AddQemu adds a qemu builder to the reservation builder +func (r *ReservationBuilder) AddQemu(qemu QemuBuilder) *ReservationBuilder { + r.reservation.DataReservation.Qemus = append(r.reservation.DataReservation.Qemus, qemu.Qemu) + return r +} + func encryptSecret(plain, nodeID string) (string, error) { if len(plain) == 0 { return "", nil