5b54cda7ac537b1734415dac1df35e5121942802 — Louis Solofrizzo 10 months ago b98475b
api, csc, sdk: Add proper quota support and basic image handling

Signed-off-by: Louis Solofrizzo <lsolofrizzo@online.net>
M api/CMakeLists.txt => api/CMakeLists.txt +1 -1
@@ 10,5 10,5 @@ admin.go
      ssh.go
      instance.go
-     cron.go
+     images.go
  )

D api/cron.go => api/cron.go +0 -61
@@ 1,61 0,0 @@-package main
- 
- import (
- 	"fmt"
- 	"github.com/lxc/lxd/shared/api"
- )
- 
- func AddIpv4ToInstances() {
- 	var entries []DbIpv4
- 
- 	DB.Find(&entries)
- 	for _, ip := range entries {
- 		found := false
- 		inst, _, _ := Context.LXD.GetInstanceState("LF" + ip.Instance)
- 		for _, net := range inst.Network {
- 			for _, addr := range net.Addresses {
- 				if addr.Address == ip.IP {
- 					found = true
- 				}
- 			}
- 		}
- 
- 		if !found {
- 			op, err := Context.LXD.ExecContainer("LF"+ip.Instance, api.ContainerExecPost{
- 				Command: []string{"/usr/bin/env", "ip", "addr", "add", ip.IP + "/32", "dev", "eth0"},
- 			}, nil)
- 
- 			if err != nil {
- 				fmt.Printf("Error: %s", err.Error())
- 				continue
- 			}
- 
- 			err = op.Wait()
- 			if err != nil {
- 				fmt.Printf("Error: %s", err.Error())
- 			}
- 		}
- 	}
- }
- 
- func SetHostnameToInstance() {
- 	var entries []DbInstance
- 
- 	DB.Find(&entries)
- 	for _, instance := range entries {
- 		fmt.Printf("Setting hostname '%s' to instance 'LF%s'\n", instance.Name, instance.Uuid)
- 		op, err := Context.LXD.ExecContainer("LF"+instance.Uuid, api.ContainerExecPost{
- 			Command: []string{"/usr/bin/env", "hostname", instance.Name},
- 		}, nil)
- 
- 		if err != nil {
- 			fmt.Printf("Error: %s", err.Error())
- 			continue
- 		}
- 
- 		err = op.Wait()
- 		if err != nil {
- 			fmt.Printf("Error: %s", err.Error())
- 		}
- 	}
- }

A api/images.go => api/images.go +54 -0
@@ 0,0 1,54 @@
+ package main
+ 
+ import (
+ 	"cisco/sdk"
+ 	"errors"
+ 	"github.com/kataras/iris"
+ )
+ 
+ func OSIsValid(ctx iris.Context, name string) error {
+ 	images, err := Context.LXD.GetImages()
+ 
+ 	if err != nil {
+ 		APIError(ctx, 503, err.Error())
+ 		return err
+ 	}
+ 
+ 	for _, img := range images {
+ 		if img.Public {
+ 			if name == img.Aliases[0].Name {
+ 				return nil
+ 			}
+ 		}
+ 	}
+ 
+ 	APIError(ctx, 400, "The specified OS is not valid")
+ 	return errors.New("The specified OS is not valid")
+ }
+ 
+ func images_list(ctx iris.Context) {
+ 	var result []cisco.Image
+ 	images, err := Context.LXD.GetImages()
+ 
+ 	if err != nil {
+ 		APIError(ctx, 503, err.Error())
+ 		return
+ 	}
+ 
+ 	for _, img := range images {
+ 		if img.Public {
+ 			result = append(result, cisco.Image{
+ 				Name:         img.Aliases[0].Name,
+ 				Architecture: img.Architecture,
+ 				Fingerprint:  img.Fingerprint,
+ 				Size:         img.Size,
+ 			})
+ 		}
+ 	}
+ 
+ 	ctx.JSON(result)
+ }
+ 
+ func InitImageRoutes(app *iris.Application) {
+ 	app.Get("/image", APIAuth, images_list)
+ }

M api/instance.go => api/instance.go +58 -75
@@ 49,78 49,9 @@ ctx.JSON(&result)
  }
  
- func OSIsValid(ctx iris.Context, name string) error {
- 	var validOs []string = []string{
- 		"alpine/3.10",
- 		"alpine/3.7",
- 		"alpine/3.8",
- 		"alpine/3.9",
- 		"alpine/edge",
- 		"alt/p8",
- 		"alt/p9",
- 		"alt/Sisyphus",
- 		"apertis/17.12",
- 		"apertis/18.03",
- 		"apertis/18.06",
- 		"apertis/18.09",
- 		"apertis/18.12",
- 		"archlinux/current",
- 		"centos/6",
- 		"centos/7",
- 		"centos/8",
- 		"debian/bullseye",
- 		"debian/buster",
- 		"debian/jessie",
- 		"debian/sid",
- 		"debian/stretch",
- 		"devuan/ascii",
- 		"fedora/29",
- 		"fedora/30",
- 		"fedora/31",
- 		"funtoo/1.3",
- 		"gentoo/current",
- 		"kali/current",
- 		"mint/sarah",
- 		"mint/serena",
- 		"mint/sonya",
- 		"mint/sylvia",
- 		"mint/tara",
- 		"mint/tessa",
- 		"mint/tina",
- 		"opensuse/15.0",
- 		"opensuse/15.1",
- 		"opensuse/tumbleweed",
- 		"openwrt/18.06",
- 		"openwrt/current",
- 		"openwrt/snapshot",
- 		"oracle/6",
- 		"oracle/7",
- 		"plamo/6.x",
- 		"plamo/7.x",
- 		"sabayon/current",
- 		"ubuntu/bionic",
- 		"ubuntu-core/16",
- 		"ubuntu/cosmic",
- 		"ubuntu/disco",
- 		"ubuntu/eoan",
- 		"ubuntu/trusty",
- 		"ubuntu/xenial",
- 		"voidlinux/current",
- 	}
- 
- 	for _, v := range validOs {
- 		if v == name {
- 			return nil
- 		}
- 	}
- 
- 	APIError(ctx, 400, "The specified OS is not valid")
- 	return errors.New("The specified OS is not valid")
- }
- 
  func ArchitectureIsValid(ctx iris.Context, name string) error {
  	var Architectures []string = []string{
- 		"intel",
+ 		"x86_64",
  	}
  
  	for _, v := range Architectures {


@@ 137,10 68,8 @@ op, err := Context.LXD.CreateContainer(api.ContainersPost{
  		Name: "LF" + instance.Uuid,
  		Source: api.ContainerSource{
- 			Type:     "image",
- 			Alias:    instance.OS,
- 			Server:   "https://images.linuxcontainers.org",
- 			Protocol: "simplestreams",
+ 			Type:  "image",
+ 			Alias: instance.OS,
  		},
  	})
  


@@ 194,7 123,17 @@ return
  	}
  
- 	// XXX: quota
+ 	if !CheckInstanceQuotas(ctx) {
+ 		return
+ 	}
+ 
+ 	if !CheckStorageQuotas(ctx, 10) {
+ 		return
+ 	}
+ 
+ 	if !CheckSSHKeys(ctx) {
+ 		return
+ 	}
  
  	entry.Uuid = uuid.NewV4().String()
  	entry.Name = instance.Name


@@ 334,10 273,54 @@ DB.Delete(&entry)
  }
  
+ func instance_name(ctx iris.Context) {
+ 	var entry DbInstance
+ 
+ 	instance := ctx.Params().Get("name")
+ 
+ 	DB.First(&entry, "uuid = ?", instance)
+ 
+ 	if entry.ID == 0 {
+ 		instance = instance[2:]
+ 		DB.First(&entry, "uuid = ?", instance)
+ 		if entry.ID == 0 {
+ 			APIError(ctx, 404, "Can't find the specified instance")
+ 			return
+ 		}
+ 	}
+ 
+ 	ctx.WriteString(entry.Name)
+ }
+ 
+ func instance_keys(ctx iris.Context) {
+ 	var entry DbInstance
+ 	var keys []DbSSHKey
+ 
+ 	instance := ctx.Params().Get("name")
+ 
+ 	DB.First(&entry, "uuid = ?", instance)
+ 
+ 	if entry.ID == 0 {
+ 		instance = instance[2:]
+ 		DB.First(&entry, "uuid = ?", instance)
+ 		if entry.ID == 0 {
+ 			APIError(ctx, 404, "Can't find the specified instance")
+ 			return
+ 		}
+ 	}
+ 
+ 	DB.Find(&keys, "user = ?", entry.User)
+ 	for _, key := range keys {
+ 		ctx.WriteString(key.Key)
+ 	}
+ }
+ 
  func InitInstanceRoutes(app *iris.Application) {
  	app.Get("/instance", APIAuth, instance_list)
  	app.Post("/instance", APIAuth, instance_new)
  	app.Put("/instance/start/{name:string}", APIAuth, instance_start)
  	app.Put("/instance/stop/{name:string}", APIAuth, instance_stop)
  	app.Delete("/instance/{name:string}", APIAuth, instance_delete)
+ 	app.Get("/instance/keys/{name:string}", instance_keys)
+ 	app.Get("/instance/name/{name:string}", instance_name)
  }

M api/invite.go => api/invite.go +2 -1
@@ 14,13 14,14 @@ You have been invited to use the %s cloud platform.
  Here's your invitation code: %s.
  
- You can either activate it via the webinterface[1], or via the command line tool:
+ You can either activate it via the webinterface[1], or via the command line tool[2]:
  
      csc invite claim %s --endpoint https://api.%s
  
  Take Care,
  
  [1] https://%s/invite/claim/%s
+ [2] https://louifox.s3.fr-par.scw.cloud/cisco/csc/latest/csc
  `
  
  func invite_new(ctx iris.Context) {

M api/main.go => api/main.go +1 -6
@@ 9,7 9,6 @@ "flag"
  	"fmt"
  	"github.com/kataras/iris"
- 	"github.com/robfig/cron"
  	"github.com/satori/go.uuid"
  	"os"
  )


@@ 148,11 147,7 @@ InitAdminRoutes(app)
  	InitSSHRoutes(app)
  	InitInstanceRoutes(app)
- 
- 	c := cron.New()
- 	c.AddFunc("@every 1m", AddIpv4ToInstances)
- 	c.AddFunc("@every 1m", SetHostnameToInstance)
- 	c.Start()
+ 	InitImageRoutes(app)
  
  	app.Run(iris.Addr(Config.Address))
  }

M api/quotas.go => api/quotas.go +74 -0
@@ 31,3 31,77 @@   	return true
  }
+ 
+ func CheckInstanceQuotas(ctx iris.Context) bool {
+ 	var quotas DbQuotas
+ 	var count int
+ 
+ 	role := ctx.Values().GetString("Role")
+ 	id := ctx.Values().GetString("User")
+ 
+ 	DB.First(&quotas, "role = ?", role)
+ 	if quotas.ID == 0 {
+ 		APIError(ctx, 500, "Could not find quotas for your role")
+ 		return false
+ 	}
+ 
+ 	if quotas.MaxInstances == -1 {
+ 		return true
+ 	}
+ 
+ 	DB.Model(&DbInstance{}).Where("user = ?", id).Count(&count)
+ 	if count >= quotas.MaxInstances {
+ 		quota := fmt.Sprintf("%d/%d", count, quotas.MaxInstances)
+ 		APIError(ctx, 403, "Your quotas for instances have been reached ("+quota+"), you cannot create another one. If you want an upgrade, please contact one of the admins")
+ 		return false
+ 	}
+ 
+ 	return true
+ }
+ 
+ func CheckStorageQuotas(ctx iris.Context, size uint) bool {
+ 	var quotas DbQuotas
+ 	var instances []DbInstance
+ 	var currentSize uint
+ 
+ 	role := ctx.Values().GetString("Role")
+ 	id := ctx.Values().GetString("User")
+ 
+ 	DB.First(&quotas, "role = ?", role)
+ 	if quotas.ID == 0 {
+ 		APIError(ctx, 500, "Could not find quotas for your role")
+ 		return false
+ 	}
+ 
+ 	if quotas.MaxStorage == -1 {
+ 		return true
+ 	}
+ 
+ 	DB.Find(&instances, "user = ?", id)
+ 	for _, inst := range instances {
+ 		currentSize += inst.Size
+ 	}
+ 
+ 	if currentSize+size > uint(quotas.MaxStorage) {
+ 		quota := fmt.Sprintf("%d/%d", currentSize, quotas.MaxStorage)
+ 		APIError(ctx, 403, "Your quotas for storage have been reached ("+quota+"). If you want an upgrade, please contact one of the admins")
+ 		return false
+ 
+ 	}
+ 
+ 	return true
+ }
+ 
+ func CheckSSHKeys(ctx iris.Context) bool {
+ 	var entries []DbSSHKey
+ 
+ 	user := ctx.Values().Get("User")
+ 	DB.Find(&entries, "user = ?", user)
+ 
+ 	if len(entries) == 0 {
+ 		APIError(ctx, 403, "Please add an SSH key to your account before creating an instance")
+ 		return false
+ 	}
+ 
+ 	return true
+ }

A csc/cmd/image.go => csc/cmd/image.go +14 -0
@@ 0,0 1,14 @@
+ package cmd
+ 
+ import (
+ 	"github.com/spf13/cobra"
+ )
+ 
+ var ImageRootCmd = &cobra.Command{
+ 	Use:   "image",
+ 	Short: "Utilities for image",
+ }
+ 
+ func init() {
+ 	RootCmd.AddCommand(ImageRootCmd)
+ }

A csc/cmd/image/list.go => csc/cmd/image/list.go +38 -0
@@ 0,0 1,38 @@
+ package image
+ 
+ import (
+ 	"cisco/csc/cmd"
+ 	"cisco/sdk"
+ 	"fmt"
+ 	"github.com/olekukonko/tablewriter"
+ 	"github.com/spf13/cobra"
+ 	"os"
+ 	"strconv"
+ )
+ 
+ var imageList = &cobra.Command{
+ 	Use:   "list",
+ 	Short: "List Images",
+ 	Args:  cobra.ExactArgs(0),
+ 	Run: func(cmd *cobra.Command, args []string) {
+ 		images, err := cisco.ListImage()
+ 
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 			return
+ 		}
+ 
+ 		table := tablewriter.NewWriter(os.Stdout)
+ 		table.SetHeader([]string{"Name", "Fingerprint", "Architecture", "Size"})
+ 
+ 		for _, img := range images {
+ 			table.Append([]string{img.Name, img.Fingerprint, img.Architecture, strconv.FormatInt(int64(img.Size), 10)})
+ 		}
+ 
+ 		table.Render()
+ 	},
+ }
+ 
+ func init() {
+ 	cmd.ImageRootCmd.AddCommand(imageList)
+ }

M csc/cmd/instance/list.go => csc/cmd/instance/list.go +2 -2
@@ 23,9 23,9 @@ }
  
  		table := tablewriter.NewWriter(os.Stdout)
- 		table.SetHeader([]string{"Name", "Status", "Ipv4", "Ipv6", "Size"})
+ 		table.SetHeader([]string{"Name", "Status", "OS", "Ipv6", "Size"})
  		for _, instance := range instances {
- 			table.Append([]string{instance.Name, instance.Status, instance.Ipv4, instance.Ipv6, strconv.FormatInt(int64(instance.Size), 10) + "GB"})
+ 			table.Append([]string{instance.Name, instance.Status, instance.OS, instance.Ipv6, strconv.FormatInt(int64(instance.Size), 10) + "GB"})
  		}
  		table.Render()
  	},

M csc/cmd/instance/new.go => csc/cmd/instance/new.go +1 -1
@@ 18,7 18,7 @@ err := cisco.NewInstance(cisco.InstanceNew{
  			Name:         name,
  			OS:           os,
- 			Architecture: "intel",
+ 			Architecture: "x86_64",
  		})
  
  		if err != nil {

M csc/main.go => csc/main.go +1 -0
@@ 6,6 6,7 @@ _ "cisco/csc/cmd/admin/quotas"
  	_ "cisco/csc/cmd/admin/quotas/update"
  	_ "cisco/csc/cmd/admin/user"
+ 	_ "cisco/csc/cmd/image"
  	_ "cisco/csc/cmd/instance"
  	_ "cisco/csc/cmd/invite"
  	_ "cisco/csc/cmd/ssh"

A sdk/image.go => sdk/image.go +25 -0
@@ 0,0 1,25 @@
+ package cisco
+ 
+ import (
+ 	"encoding/json"
+ )
+ 
+ type Image struct {
+ 	Name         string `json:"name"`
+ 	Architecture string `json:"architecture"`
+ 	Fingerprint  string `json:"fingerprint"`
+ 	Size         int64  `json:"size"`
+ }
+ 
+ func ListImage() ([]Image, error) {
+ 	var result []Image
+ 
+ 	data, err := SendRequest(ImageListRoute, Request{})
+ 
+ 	if err != nil {
+ 		return nil, err
+ 	}
+ 
+ 	json.Unmarshal(data.([]byte), &result)
+ 	return result, err
+ }

M sdk/request.go => sdk/request.go +6 -0
@@ 164,6 164,12 @@ Protected: true,
  }
  
+ var ImageListRoute Route = Route{
+ 	Path:      "/image",
+ 	Method:    "GET",
+ 	Protected: true,
+ }
+ 
  type Request struct {
  	Arg  string
  	Data interface{}