Skip to content

Commit f4cfdd9

Browse files
authored
Merge pull request #1 from anastop/provision-with-initd
Add init.d script to provision command
2 parents ee3a2d3 + 3beb9e7 commit f4cfdd9

File tree

8 files changed

+117
-42
lines changed

8 files changed

+117
-42
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# virgo
22

3-
Flexible provisioning and management of virtual machines
3+
virgo allows you to quickly provision and spin VMs locally, leveraging cloud-init and
4+
Libvirt.
45

56
![virgo](./virgo.png)
67

@@ -11,9 +12,11 @@ Flexible provisioning and management of virtual machines
1112
- Allows easy VM creation with flexible configuration options
1213
- Supports [vhost-user network interfaces](https://libvirt.org/formatdomain.html#elementVhostuser), to allow a VM to connect e.g. with a DPDK-based vswitch
1314

14-
Provisioning options:
15+
Provisioning options:
1516
- cloud image used for provisioning (currently tested with Ubuntu 16.04)
1617
- user credentials
18+
- custom provisioning script to be used during VM creation
19+
- custom init.d script to be installed permanently
1720

1821
VM configuration options:
1922
- number of vCPUs
@@ -38,7 +41,7 @@ virgo makes use of the following utilities:
3841

3942
Provision a new VM called "foo":
4043
```console
41-
$ sudo virgo --config guest_config.json --script provision.sh --guest foo
44+
$ sudo virgo provision --config guest_config.json --provision-script provision.sh --initd-script initd.sh --guest foo
4245
```
4346
"foo" will shutdown after provisioning.
4447

cmd/provision.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ The bash script can be any valid bash script and is executed with root permissio
2424
return fmt.Errorf("failed to parse 'guest' argument: %v", err)
2525
}
2626

27-
scriptFile, err := cmd.Flags().GetString("script")
27+
provisionScript, err := cmd.Flags().GetString("provision-script")
2828
if err != nil {
29-
return fmt.Errorf("failed to parse script argument: %v", err)
29+
return fmt.Errorf("failed to parse provision argument: %v", err)
30+
}
31+
32+
initdScript, err := cmd.Flags().GetString("initd-script")
33+
if err != nil {
34+
return fmt.Errorf("failed to parse initd argument: %v", err)
3035
}
3136

3237
conf, err := cmd.Flags().GetString("config")
@@ -43,19 +48,29 @@ The bash script can be any valid bash script and is executed with root permissio
4348
if err := json.Unmarshal(data, pc); err != nil {
4449
return fmt.Errorf("failed to unmarshal provision config: %v", err)
4550
}
51+
pc.Name = guest
4652

4753
gc := &virgo.GuestConf{}
4854
if err := json.Unmarshal(data, gc); err != nil {
4955
return fmt.Errorf("failed to unmarshal guest config: %v", err)
5056
}
5157
gc.Name = guest
5258

53-
data, err = ioutil.ReadFile(scriptFile)
59+
data, err = ioutil.ReadFile(provisionScript)
5460
if err != nil {
55-
return fmt.Errorf("failed to read provision script %s: %v", scriptFile, err)
61+
return fmt.Errorf("failed to read provision script %s: %v", provisionScript, err)
5662
}
5763
pc.Provision = string(data)
5864

65+
66+
if initdScript != "" {
67+
data, err = ioutil.ReadFile(initdScript)
68+
if err != nil {
69+
return fmt.Errorf("failed to read initd script %s: %v", initdScript, err)
70+
}
71+
pc.Initd = string(data)
72+
}
73+
5974
l, err := virgo.NewLibvirtConn()
6075
if err != nil {
6176
return fmt.Errorf("failed to open Libvirt connection: %v", err)
@@ -76,7 +91,10 @@ The bash script can be any valid bash script and is executed with root permissio
7691

7792
func init() {
7893
provisionCmd.Flags().StringP("guest", "g", "", "guest to provision")
79-
provisionCmd.Flags().StringP("script", "s", "", "bash script to be used for provisioning")
94+
provisionCmd.Flags().StringP("provision-script", "p", "", "bash script to be used for provisioning")
95+
provisionCmd.Flags().StringP("initd-script", "i", "", "bash script to be used in init.d")
8096
provisionCmd.Flags().StringP("config", "c", "", "JSON file containing the provisioning options")
97+
provisionCmd.MarkFlagRequired("config")
98+
provisionCmd.MarkFlagRequired("guest")
8199
rootCmd.AddCommand(provisionCmd)
82100
}

cmd/purge.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ var purgeCmd = &cobra.Command{
4141

4242
func init() {
4343
purgeCmd.Flags().StringP("guest", "g", "", "guest to purge")
44+
purgeCmd.MarkFlagRequired("guest")
4445
rootCmd.AddCommand(purgeCmd)
4546
}

cmd/start.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ either via 'provision' or 'launch'`,
3939

4040
func init() {
4141
startCmd.Flags().StringP("guest", "g", "", "guest to start")
42+
startCmd.MarkFlagRequired("guest")
4243
rootCmd.AddCommand(startCmd)
4344
}

cmd/stop.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ var stopCmd = &cobra.Command{
3737

3838
func init() {
3939
stopCmd.Flags().StringP("guest", "g", "", "guest to stop")
40+
stopCmd.MarkFlagRequired("guest")
4041
rootCmd.AddCommand(stopCmd)
4142
}

cmd/undefine.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ If it's running, the domain is first stopped.'`,
3939

4040
func init() {
4141
undefineCmd.Flags().StringP("guest", "g", "", "guest to undefine")
42+
undefineCmd.MarkFlagRequired("guest")
4243
rootCmd.AddCommand(undefineCmd)
4344
}

pkg/virgo/virgo.go

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,27 @@ import (
1818

1919
var metaDataFmt = `
2020
instance-id: iid-%s;
21-
#hostname: %s
22-
#local-hostname: %s`
21+
`
2322

24-
var userDataFmt = `#cloud-config
23+
var userDataTmpl = `#cloud-config
2524
users:
26-
- name: %s
25+
- name: {{.User}}
2726
lock_passwd: false
2827
sudo: ALL=(ALL) NOPASSWD:ALL
2928
shell: /bin/bash
3029
# this is the outcome of the command openssl passwd -1 -salt SaltSalt $PASSWORD
31-
passwd: %s
30+
passwd: {{.PasswdHash}}
3231
3332
write_files:
3433
- path: /provision.sh
3534
content: |
36-
%s
35+
{{.Provision | indentByFour }}
3736
37+
{{- if ne .Initd "" }}
38+
- path: /etc/init.d/{{.Name}}
39+
content: |
40+
{{.Initd | indentByFour }}
41+
{{- end}}
3842
3943
- path: /remove_cloud_init.sh
4044
content: |
@@ -44,7 +48,7 @@ write_files:
4448
sudo rm -rf /etc/cloud/; sudo rm -rf /var/lib/cloud/
4549
4650
47-
password: %s
51+
#password: {{.Passwd}}
4852
chpasswd: { expire: False }
4953
ssh_pwauth: True
5054
@@ -56,6 +60,10 @@ apt_upgrade: true
5660
5761
runcmd:
5862
- bash /provision.sh
63+
{{- if ne .Initd "" }}
64+
- chmod +x /etc/init.d/{{.Name}}
65+
- update-rc.d {{.Name}} defaults
66+
{{- end}}
5967
- bash /remove_cloud_init.sh
6068
- shutdown
6169
@@ -64,12 +72,15 @@ power_state:
6472
`
6573

6674
type ProvisionConf struct {
75+
Name string `json:"name"`
6776
CloudImgURL string `json:"cloud_img_url"`
6877
CloudImgName string `json:"cloud_img_name"`
6978
User string `json:"user"`
7079
Passwd string `json:"passwd"`
7180
RootImgGB int `json:"root_img_gb"`
7281
Provision string
82+
Initd string
83+
PasswdHash string
7384
}
7485

7586
type NetIf struct {
@@ -94,40 +105,62 @@ type GuestConf struct {
94105
}
95106

96107
func createMetaDataFile(path, guest string) error {
97-
s := fmt.Sprintf(metaDataFmt, guest, guest, guest)
108+
s := fmt.Sprintf(metaDataFmt, guest)
98109
if err := ioutil.WriteFile(path, []byte(s), 0644); err != nil {
99110
return err
100111
}
101112
return nil
102113
}
103114

104-
func createUserDataFile(path, user, passwd, script string) error {
105-
cmd := exec.Command("openssl", "passwd", "-1", "-salt", "SaltSalt", passwd)
106-
hash, err := cmd.CombinedOutput()
115+
func indentByFour(s string) string {
116+
return regexp.MustCompile("\n").ReplaceAllString(s, "\n ")
117+
}
118+
119+
func userData(p *ProvisionConf) (string, error) {
120+
t, err := template.New("udtmpl").
121+
Funcs(template.FuncMap{"indentByFour": indentByFour}).
122+
Parse(userDataTmpl)
123+
if err != nil {
124+
return "", fmt.Errorf("failed to parse template: %v", err)
125+
}
126+
127+
var xml bytes.Buffer
128+
if err := t.Execute(&xml, p); err != nil {
129+
return "", fmt.Errorf("failed to execute template: %v", err)
130+
}
131+
132+
return xml.String(), nil
133+
}
134+
135+
func createUserDataFile(path string, p *ProvisionConf) error {
136+
cmd := exec.Command("openssl", "passwd", "-1", "-salt", "SaltSalt", p.Passwd)
137+
out, err := cmd.CombinedOutput()
107138
if err != nil {
108139
return fmt.Errorf("failed to executed %v: %v", cmd.Args, err)
109140
}
110141

111-
re := regexp.MustCompile("\n")
112-
indentedScript := re.ReplaceAllString(script, "\n ")
113-
s := fmt.Sprintf(userDataFmt, user, hash, indentedScript, passwd)
142+
p.PasswdHash = string(out)
143+
144+
s, err := userData(p)
145+
if err != nil {
146+
return fmt.Errorf("failed to create user-data string from config %+v: %v", p, err)
147+
}
114148

115149
if err := ioutil.WriteFile(path, []byte(s), 0644); err != nil {
116150
return err
117151
}
118152
return nil
119153
}
120154

121-
func createConfigIsoImage(path, guest, user, passwd, prov string) error {
122-
var metaDataPath = "meta-data"
123-
var userDataPath = "user-data"
124-
125-
if err := createMetaDataFile(metaDataPath, guest); err != nil {
155+
func createConfigIsoImage(path string, p *ProvisionConf) error {
156+
userDataPath := "user-data"
157+
metaDataPath := "meta-data"
158+
if err := createMetaDataFile(metaDataPath, p.Name); err != nil {
126159
return fmt.Errorf("failed to create meta-data file for cloud-init: %v", err)
127160
}
128161
//defer os.Remove(metaDataPath)
129162

130-
if err := createUserDataFile(userDataPath, user, passwd, prov); err != nil {
163+
if err := createUserDataFile(userDataPath, p); err != nil {
131164
return fmt.Errorf("failed to create user-data file for cloud-init: %v", err)
132165
}
133166
//defer os.Remove(userDataPath)
@@ -168,13 +201,13 @@ var domTmpl = `
168201
<currentMemory unit='MiB'>{{.MemoryMB}}</currentMemory>
169202
170203
<!-- hugepages -->
171-
{{if .HugepageSupport}}
204+
{{- if .HugepageSupport}}
172205
<memoryBacking>
173206
<hugepages>
174207
<page size='{{.HugepageSize}}' unit='{{.HugepageSizeUnit}}' nodeset='{{.HugepageNodeSet}}'/>
175208
</hugepages>
176209
</memoryBacking>
177-
{{end}}
210+
{{- end}}
178211
179212
<vcpu placement='static'>{{.NumVcpus}}</vcpu>
180213
<!-- cputune><shares>4096</shares>
@@ -322,7 +355,7 @@ func GuestImagePaths(l *libvirt.Libvirt, poolName, guest string) (rootImgPath, c
322355
return
323356
}
324357

325-
func createVolumes(l *libvirt.Libvirt, guest string, c *ProvisionConf) (rootImgPath, configIsoPath string, e error) {
358+
func createVolumes(l *libvirt.Libvirt, c *ProvisionConf) (rootImgPath, configIsoPath string, e error) {
326359
baseu, err := url.Parse(c.CloudImgURL)
327360
if err != nil {
328361
e = err
@@ -341,18 +374,18 @@ func createVolumes(l *libvirt.Libvirt, guest string, c *ProvisionConf) (rootImgP
341374
return
342375
}
343376

344-
rootImgPath, configIsoPath, err = GuestImagePaths(l, DefaultPool(), guest)
377+
rootImgPath, configIsoPath, err = GuestImagePaths(l, DefaultPool(), c.Name)
345378
if err != nil {
346379
e = fmt.Errorf("failed to compute guest image paths: %v", err)
347380
return
348381
}
349382

350-
if err := createConfigIsoImage(ConfigIsoName(guest), guest, c.User, c.Passwd, c.Provision); err != nil {
351-
e = fmt.Errorf("failed to create configuration iso image %s: %v", ConfigIsoName, err)
383+
if err := createConfigIsoImage(ConfigIsoName(c.Name), c); err != nil {
384+
e = fmt.Errorf("failed to create configuration iso image %s: %v", ConfigIsoName(c.Name), err)
352385
return
353386
}
354387

355-
if err := copyFile(ConfigIsoName(guest), configIsoPath); err != nil {
388+
if err := copyFile(ConfigIsoName(c.Name), configIsoPath); err != nil {
356389
e = fmt.Errorf("failed to copy configuration iso under storage pool's directory: %v", err)
357390
return
358391
}
@@ -373,9 +406,9 @@ func createVolumes(l *libvirt.Libvirt, guest string, c *ProvisionConf) (rootImgP
373406
return
374407
}
375408

376-
vol, err := l.StorageVolLookupByName(pool, RootImgName(guest))
409+
vol, err := l.StorageVolLookupByName(pool, RootImgName(c.Name))
377410
if err != nil {
378-
e = fmt.Errorf("failed to lookup storage volume %s under pool %s: %v", RootImgName, pool.Name, err)
411+
e = fmt.Errorf("failed to lookup storage volume %s under pool %s: %v", RootImgName(c.Name), pool.Name, err)
379412
return
380413
}
381414

@@ -422,7 +455,7 @@ func ConfigIsoName(guest string) string {
422455

423456
func Provision(l *libvirt.Libvirt, p *ProvisionConf, g *GuestConf) error {
424457
var err error
425-
g.RootImgPath, g.ConfigIsoPath, err = createVolumes(l, g.Name, p)
458+
g.RootImgPath, g.ConfigIsoPath, err = createVolumes(l, p)
426459
if err != nil {
427460
return fmt.Errorf("failed to create volumes: %v", err)
428461
}

pkg/virgo/virgo_test.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ func TestDomXMLStr(t *testing.T) {
1212
HugepageSize: 2,
1313
HugepageSizeUnit: "MB",
1414
HugepageNodeSet: "0",
15-
rootImgPath: "foo.img",
16-
configIsoPath: "foo.iso",
15+
RootImgPath: "foo.img",
16+
ConfigIsoPath: "foo.iso",
1717
NetIfs: []NetIf{
1818
{Type: "bridge", Bridge: "virbr0"},
1919
{Type: "bridge", Bridge: "virbr0"},
@@ -32,11 +32,28 @@ func TestDomXMLStr(t *testing.T) {
3232
},
3333
}
3434

35-
t.Logf("Domain XML string: %s", domXMLStr("foo", s))
35+
xml, err := domXML(s)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
t.Logf("Domain XML string: %s", xml)
3640
}
3741

3842
func TestCreateUserDataFile(t *testing.T) {
39-
if err := createUserDataFile("user-data", "nfvsap", "nfvsap", "echo hello\nls *"); err != nil {
43+
p := &ProvisionConf{Name: "test",
44+
CloudImgURL: "https://cloud-images.ubuntu.com/releases/16.04/release/",
45+
CloudImgName: "ubuntu-16.04-server-cloudimg-amd64-disk1.img",
46+
User: "nfvsap",
47+
Passwd: "nfvsap",
48+
}
49+
50+
p.Provision = `#/bin/bash
51+
echo Hello
52+
echo Hello again`
53+
54+
p.Initd = p.Provision
55+
56+
if err := createUserDataFile("user-data", p); err != nil {
4057
t.Fatal(err)
4158
}
4259
}

0 commit comments

Comments
 (0)