Simple application VMs (hypervisor-based sandbox) based on Nix package manager.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

appvm.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /**
  2. * @author Mikhail Klementev jollheef<AT>riseup.net
  3. * @license GNU GPLv3
  4. * @date July 2018
  5. * @brief appvm launcher
  6. */
  7. package main
  8. import (
  9. "errors"
  10. "fmt"
  11. "io"
  12. "io/ioutil"
  13. "log"
  14. "math/rand"
  15. "net"
  16. "os"
  17. "os/exec"
  18. "path/filepath"
  19. "regexp"
  20. "strconv"
  21. "strings"
  22. "syscall"
  23. "time"
  24. "github.com/digitalocean/go-libvirt"
  25. "github.com/go-cmd/cmd"
  26. "github.com/jollheef/go-system"
  27. "github.com/olekukonko/tablewriter"
  28. kingpin "gopkg.in/alecthomas/kingpin.v2"
  29. )
  30. type networkModel int
  31. const (
  32. networkOffline networkModel = iota
  33. networkQemu networkModel = iota
  34. networkLibvirt networkModel = iota
  35. )
  36. func list(l *libvirt.Libvirt) {
  37. domains, err := l.Domains()
  38. if err != nil {
  39. log.Fatal(err)
  40. }
  41. fmt.Println("Started VM:")
  42. for _, d := range domains {
  43. if strings.HasPrefix(d.Name, "appvm") {
  44. fmt.Println("\t", d.Name[6:])
  45. }
  46. }
  47. fmt.Println("\nAvailable VM:")
  48. files, err := ioutil.ReadDir(configDir + "/nix")
  49. if err != nil {
  50. log.Fatal(err)
  51. }
  52. for _, f := range files {
  53. switch f.Name() {
  54. case "base.nix":
  55. continue
  56. case "local.nix":
  57. continue
  58. }
  59. fmt.Println("\t", f.Name()[0:len(f.Name())-4])
  60. }
  61. }
  62. func copyFile(from, to string) (err error) {
  63. source, err := os.Open(from)
  64. if err != nil {
  65. return
  66. }
  67. defer source.Close()
  68. destination, err := os.Create(to)
  69. if err != nil {
  70. return
  71. }
  72. _, err = io.Copy(destination, source)
  73. if err != nil {
  74. destination.Close()
  75. return
  76. }
  77. return destination.Close()
  78. }
  79. func prepareTemplates(appvmPath string) (err error) {
  80. if _, err = os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
  81. err = ioutil.WriteFile(configDir+"/nix/local.nix", local_nix_template, 0644)
  82. if err != nil {
  83. return
  84. }
  85. }
  86. return
  87. }
  88. func streamStdOutErr(command *cmd.Cmd) {
  89. for {
  90. select {
  91. case line := <-command.Stdout:
  92. fmt.Println(line)
  93. case line := <-command.Stderr:
  94. fmt.Fprintln(os.Stderr, line)
  95. }
  96. }
  97. }
  98. func generateVM(path, name string, verbose bool) (realpath, reginfo, qcow2 string, err error) {
  99. command := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true},
  100. "nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
  101. "-I", "nixos-config="+path+"/nix/"+name+".nix", "-I", path)
  102. if verbose {
  103. go streamStdOutErr(command)
  104. }
  105. status := <-command.Start()
  106. if status.Error != nil || status.Exit != 0 {
  107. log.Println(status.Error, status.Stdout, status.Stderr)
  108. if status.Error != nil {
  109. err = status.Error
  110. } else {
  111. s := fmt.Sprintf("ret code: %d, out: %v, err: %v",
  112. status.Exit, status.Stdout, status.Stderr)
  113. err = errors.New(s)
  114. }
  115. return
  116. }
  117. realpath, err = filepath.EvalSymlinks("result/system")
  118. if err != nil {
  119. return
  120. }
  121. matches, err := filepath.Glob("result/bin/run-*-vm")
  122. if err != nil || len(matches) != 1 {
  123. return
  124. }
  125. bytes, err := ioutil.ReadFile(matches[0])
  126. if err != nil || len(matches) != 1 {
  127. return
  128. }
  129. match := regexp.MustCompile("regInfo=.*/registration").FindSubmatch(bytes)
  130. if len(match) != 1 {
  131. err = errors.New("should be one reginfo")
  132. return
  133. }
  134. reginfo = string(match[0])
  135. syscall.Unlink("result")
  136. qcow2 = os.Getenv("HOME") + "/appvm/.fake.qcow2"
  137. if _, e := os.Stat(qcow2); os.IsNotExist(e) {
  138. system.System("qemu-img", "create", "-f", "qcow2", qcow2, "40M")
  139. }
  140. return
  141. }
  142. func isRunning(l *libvirt.Libvirt, name string) bool {
  143. _, err := l.DomainLookupByName("appvm_" + name) // yep, there is no libvirt error handling
  144. // VM is destroyed when stop so NO VM means STOPPED
  145. return err == nil
  146. }
  147. func generateAppVM(l *libvirt.Libvirt,
  148. nixName, vmName, appvmPath, sharedDir string,
  149. verbose bool, network networkModel, gui bool) (err error) {
  150. realpath, reginfo, qcow2, err := generateVM(appvmPath, nixName, verbose)
  151. if err != nil {
  152. return
  153. }
  154. xml := generateXML(vmName, network, gui, realpath, reginfo, qcow2, sharedDir)
  155. _, err = l.DomainCreateXML(xml, libvirt.DomainStartValidate)
  156. return
  157. }
  158. func stupidProgressBar() {
  159. const length = 70
  160. for {
  161. time.Sleep(time.Second / 4)
  162. fmt.Printf("\r%s]\r[", strings.Repeat(" ", length))
  163. for i := 0; i <= length-2; i++ {
  164. time.Sleep(time.Second / 20)
  165. fmt.Printf("+")
  166. }
  167. }
  168. }
  169. func fileExists(filename string) bool {
  170. info, err := os.Stat(filename)
  171. if os.IsNotExist(err) {
  172. return false
  173. }
  174. return !info.IsDir()
  175. }
  176. func isAppvmConfigurationExists(appvmPath, name string) bool {
  177. return fileExists(appvmPath + "/nix/" + name + ".nix")
  178. }
  179. func start(l *libvirt.Libvirt, name string, verbose bool, network networkModel,
  180. gui, stateless bool, args, open string) {
  181. appvmPath := configDir
  182. statelessName := fmt.Sprintf("tmp_%d_%s", rand.Int(), name)
  183. sharedDir := os.Getenv("HOME") + "/appvm/"
  184. if stateless {
  185. sharedDir += statelessName
  186. } else {
  187. sharedDir += name
  188. }
  189. os.MkdirAll(sharedDir, 0700)
  190. vmName := "appvm_"
  191. if stateless {
  192. vmName += statelessName
  193. } else {
  194. vmName += name
  195. }
  196. if open != "" {
  197. filename := sharedDir + "/" + filepath.Base(open)
  198. err := copyFile(open, filename)
  199. if err != nil {
  200. log.Println("Can't copy file")
  201. return
  202. }
  203. args += "/home/user/" + filepath.Base(open)
  204. }
  205. if args != "" {
  206. err := ioutil.WriteFile(sharedDir+"/"+".args", []byte(args), 0700)
  207. if err != nil {
  208. log.Println("Can't write args")
  209. return
  210. }
  211. }
  212. if !isAppvmConfigurationExists(appvmPath, name) {
  213. log.Println("No configuration exists for app, " +
  214. "trying to generate")
  215. err := generate(name, "", "", false)
  216. if err != nil {
  217. log.Println("Can't auto generate")
  218. return
  219. }
  220. }
  221. if !isRunning(l, vmName) {
  222. if !verbose {
  223. go stupidProgressBar()
  224. }
  225. err := generateAppVM(l, name, vmName, appvmPath, sharedDir,
  226. verbose, network, gui)
  227. if err != nil {
  228. log.Fatal(err)
  229. }
  230. }
  231. if gui {
  232. cmd := exec.Command("virt-viewer", "-c", "qemu:///system", vmName)
  233. cmd.Start()
  234. }
  235. }
  236. func stop(l *libvirt.Libvirt, name string) {
  237. dom, err := l.DomainLookupByName("appvm_" + name)
  238. if err != nil {
  239. if libvirt.IsNotFound(err) {
  240. log.Println("Appvm not found or already stopped")
  241. return
  242. } else {
  243. log.Fatal(err)
  244. }
  245. }
  246. err = l.DomainShutdown(dom)
  247. if err != nil {
  248. log.Fatal(err)
  249. }
  250. }
  251. func drop(name string) {
  252. appDataPath := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
  253. os.RemoveAll(appDataPath)
  254. }
  255. func autoBalloon(l *libvirt.Libvirt, memoryMin, adjustPercent uint64) {
  256. domains, err := l.Domains()
  257. if err != nil {
  258. log.Fatal(err)
  259. }
  260. table := tablewriter.NewWriter(os.Stdout)
  261. table.SetHeader([]string{"Application VM", "Used memory", "Current memory", "Max memory", "New memory"})
  262. for _, d := range domains {
  263. if strings.HasPrefix(d.Name, "appvm_") {
  264. name := d.Name[6:]
  265. memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
  266. if err != nil {
  267. log.Println(err)
  268. continue
  269. }
  270. if len(memoryUsedRaw) == 0 {
  271. log.Println("Empty .memory_used file for domain", name)
  272. continue
  273. }
  274. memoryUsedMiB, err := strconv.Atoi(string(memoryUsedRaw[0 : len(memoryUsedRaw)-1]))
  275. if err != nil {
  276. log.Println(err)
  277. continue
  278. }
  279. memoryUsed := memoryUsedMiB * 1024
  280. _, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
  281. if err != nil {
  282. log.Println(err)
  283. continue
  284. }
  285. memoryNew := uint64(float64(memoryUsed) * (1 + float64(adjustPercent)/100))
  286. if memoryNew > memoryMax {
  287. memoryNew = memoryMax - 1
  288. }
  289. if memoryNew < memoryMin {
  290. memoryNew = memoryMin
  291. }
  292. err = l.DomainSetMemory(d, memoryNew)
  293. if err != nil {
  294. log.Println(err)
  295. continue
  296. }
  297. table.Append([]string{name,
  298. fmt.Sprintf("%d", memoryUsed),
  299. fmt.Sprintf("%d", memoryCurrent),
  300. fmt.Sprintf("%d", memoryMax),
  301. fmt.Sprintf("%d", memoryNew)})
  302. }
  303. }
  304. table.Render()
  305. }
  306. func search(name string) {
  307. command := exec.Command("nix", "search", name)
  308. bytes, err := command.Output()
  309. if err != nil {
  310. return
  311. }
  312. for _, line := range strings.Split(string(bytes), "\n") {
  313. fmt.Println(line)
  314. }
  315. return
  316. }
  317. func sync() {
  318. err := exec.Command("nix-channel", "--update").Run()
  319. if err != nil {
  320. log.Fatalln(err)
  321. }
  322. err = exec.Command("nix", "search", "-u").Run()
  323. if err != nil {
  324. log.Fatalln(err)
  325. }
  326. log.Println("Done")
  327. }
  328. func cleanupStatelessVMs(l *libvirt.Libvirt) {
  329. domains, err := l.Domains()
  330. if err != nil {
  331. log.Fatal(err)
  332. }
  333. dirs, err := ioutil.ReadDir(appvmHomesDir)
  334. if err != nil {
  335. log.Fatal(err)
  336. }
  337. for _, f := range dirs {
  338. if !strings.HasPrefix(f.Name(), "tmp_") {
  339. continue
  340. }
  341. alive := false
  342. for _, d := range domains {
  343. if d.Name == "appvm_"+f.Name() {
  344. alive = true
  345. }
  346. }
  347. if !alive {
  348. os.RemoveAll(appvmHomesDir + f.Name())
  349. }
  350. }
  351. }
  352. func parseNetworkModel(flagOffline bool, flagNetworking string) networkModel {
  353. if flagNetworking != "" && flagOffline {
  354. log.Fatal("Can't use both --network and --offline switches")
  355. }
  356. if flagOffline || flagNetworking == "offline" {
  357. return networkOffline
  358. }
  359. if flagNetworking == "libvirt" {
  360. return networkLibvirt
  361. }
  362. if flagNetworking == "qemu" {
  363. return networkQemu
  364. }
  365. return networkQemu // qemu is the default network model
  366. }
  367. var configDir = os.Getenv("HOME") + "/.config/appvm/"
  368. var appvmHomesDir = os.Getenv("HOME") + "/appvm/"
  369. func main() {
  370. rand.Seed(time.Now().UnixNano())
  371. os.Mkdir(os.Getenv("HOME")+"/appvm", 0700)
  372. os.MkdirAll(configDir+"/nix", 0700)
  373. err := writeBuiltinApps(configDir + "/nix")
  374. if err != nil {
  375. log.Fatal(err)
  376. }
  377. err = ioutil.WriteFile(configDir+"/nix/base.nix", baseNix(), 0644)
  378. if err != nil {
  379. log.Fatal(err)
  380. }
  381. // Copy templates
  382. err = prepareTemplates(configDir)
  383. if err != nil {
  384. log.Fatal(err)
  385. }
  386. kingpin.Command("list", "List applications")
  387. autoballonCommand := kingpin.Command("autoballoon", "Automatically adjust/reduce app vm memory")
  388. minMemory := autoballonCommand.Flag("min-memory", "Set minimal memory (megabytes)").Default("1024").Uint64()
  389. adjustPercent := autoballonCommand.Flag("adj-memory", "Adjust memory amount (percents)").Default("20").Uint64()
  390. startCommand := kingpin.Command("start", "Start application")
  391. startName := startCommand.Arg("name", "Application name").Required().String()
  392. startQuiet := startCommand.Flag("quiet", "Less verbosity").Bool()
  393. startArgs := startCommand.Flag("args", "Command line arguments").String()
  394. startOpen := startCommand.Flag("open", "Pass file to application").String()
  395. startOffline := startCommand.Flag("offline", "Disconnect").Bool()
  396. startCli := startCommand.Flag("cli", "Disable graphics mode, enable serial").Bool()
  397. startStateless := startCommand.Flag("stateless", "Do not use default state directory").Bool()
  398. startNetwork := startCommand.Flag("network", "Used networking model").Enum("offline", "qemu", "libvirt")
  399. stopName := kingpin.Command("stop", "Stop application").Arg("name", "Application name").Required().String()
  400. dropName := kingpin.Command("drop", "Remove application data").Arg("name", "Application name").Required().String()
  401. generateCommand := kingpin.Command("generate", "Generate appvm definition")
  402. generateName := generateCommand.Arg("name", "Nix package name").Required().String()
  403. generateBin := generateCommand.Arg("bin", "Binary").Default("").String()
  404. generateVMName := generateCommand.Flag("vm", "Use VM Name").Default("").String()
  405. generateBuildVM := generateCommand.Flag("build", "Build VM").Bool()
  406. searchCommand := kingpin.Command("search", "Search for application")
  407. searchName := searchCommand.Arg("name", "Application name").Required().String()
  408. kingpin.Command("sync", "Synchronize remote repos for applications")
  409. var l *libvirt.Libvirt
  410. if kingpin.Parse() != "generate" {
  411. c, err := net.DialTimeout(
  412. "unix",
  413. "/var/run/libvirt/libvirt-sock",
  414. time.Second,
  415. )
  416. if err != nil {
  417. log.Fatal(err)
  418. }
  419. l = libvirt.New(c)
  420. if err := l.Connect(); err != nil {
  421. log.Fatal(err)
  422. }
  423. defer l.Disconnect()
  424. cleanupStatelessVMs(l)
  425. }
  426. switch kingpin.Parse() {
  427. case "list":
  428. list(l)
  429. case "search":
  430. search(*searchName)
  431. case "generate":
  432. generate(*generateName, *generateBin, *generateVMName,
  433. *generateBuildVM)
  434. case "start":
  435. networkModel := parseNetworkModel(*startOffline, *startNetwork)
  436. start(l, *startName,
  437. !*startQuiet, networkModel, !*startCli, *startStateless,
  438. *startArgs, *startOpen)
  439. case "stop":
  440. stop(l, *stopName)
  441. case "drop":
  442. drop(*dropName)
  443. case "autoballoon":
  444. autoBalloon(l, *minMemory*1024, *adjustPercent)
  445. case "sync":
  446. sync()
  447. }
  448. }