diff --git a/cmd/install.go b/cmd/install.go deleted file mode 100644 index 17c1a30..0000000 --- a/cmd/install.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright © 2022 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "fmt" - - "github.com/labring/sealvm/pkg/install" - "github.com/labring/sealvm/pkg/utils/logger" - v1 "github.com/labring/sealvm/types/api/v1" - - "github.com/spf13/cobra" -) - -func newInstallCmd() *cobra.Command { - var vmType string - var installer install.Interface - installCmd := &cobra.Command{ - Use: "install", - Short: "install vm tools", - Run: func(cmd *cobra.Command, args []string) { - if installer.IsInstall() { - logger.Info("kubernetes is installed") - return - } - err := installer.Install() - if err != nil { - logger.Error(err) - } - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - installer = install.NewInstaller(vmType) - if installer == nil { - return fmt.Errorf("vm type %s not support", vmType) - } - return nil - }, - } - installCmd.Flags().StringVarP(&vmType, "type", "t", v1.MultipassType, "choose a type of infra, multipass") - installCmd.Flags().BoolVarP(&install.AutoDownload, "download", "d", true, "auto download vm tools online") - return installCmd -} - -func init() { - rootCmd.AddCommand(newInstallCmd()) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // installCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // installCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} - -func checkInstall(vmType string) error { - installer := install.NewInstaller(vmType) - if installer == nil { - return fmt.Errorf("vm type %s not support", vmType) - } - if !installer.IsInstall() { - return fmt.Errorf("vm tools %s is not installed, please use `sealvm install` retry", vmType) - } - return nil -} diff --git a/cmd/reset.go b/cmd/reset.go index 944e7c2..88d91a0 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -46,7 +46,7 @@ func newResetCmd() *cobra.Command { return errors.New("cancelled") } } - if err := checkInstall(vm.Spec.Type); err != nil { + if err := checkProvider(); err != nil { return err } t := metav1.Now() @@ -55,7 +55,6 @@ func newResetCmd() *cobra.Command { }, } resetCmd.Flags().StringVarP(&vm.Name, "name", "n", "default", "name of cluster to applied init action") - resetCmd.Flags().StringVarP(&vm.Spec.Type, "type", "t", v1.MultipassType, "choose a type of infra, multipass") return resetCmd } func init() { diff --git a/cmd/root.go b/cmd/root.go index 3009d12..1d2cdec 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/labring/sealvm/pkg/system" "github.com/labring/sealvm/pkg/template" + "github.com/labring/sealvm/pkg/utils/exec" "os" "path" "runtime" @@ -81,7 +82,6 @@ func init() { { Message: "System Management Commands:", Commands: []*cobra.Command{ - newInstallCmd(), system.NewConfigCmd(), template.NewTemplateCmd(), template.NewValuesCmd(), @@ -109,3 +109,12 @@ func onBootOnDie() { } logger.CfgConsoleAndFileLogger(debug, path.Join(clusterRootDir, "logs"), "sealvm", false) } + +func checkProvider() error { + defaultProvider, _ := system.Get(system.DefaultProvider) + logger.Debug("default provider is %s", defaultProvider) + if p := exec.ExecutableFilePath(defaultProvider); p == "" { + return fmt.Errorf("provider %s not found", defaultProvider) + } + return nil +} diff --git a/cmd/run.go b/cmd/run.go index 2b5bee9..be73650 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -42,6 +42,7 @@ func newRunCmd() *cobra.Command { Use: "run", Short: "Run cloud native vm nodes", Example: `sealvm run -n node:2,master:1`, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { applier, err := apply.NewApplierFromArgs(&vm) if err != nil { @@ -50,13 +51,16 @@ func newRunCmd() *cobra.Command { return applier.Apply() }, PreRunE: func(cmd *cobra.Command, args []string) error { + if err := checkProvider(); err != nil { + return err + } system.List() template.NewTpl().List() template.NewValues().List() if len(args) != 0 { defaultImage = args[0] } else { - newDefaultImage, err := apply.GetDefaultImage() + newDefaultImage, err := system.GetDefaultImage() if err != nil { return err } @@ -76,9 +80,6 @@ func newRunCmd() *cobra.Command { if strings.Contains(vm.Name, "-") { return fmt.Errorf("your cluster name contains chart '-' ") } - if err := checkInstall(vm.Spec.Type); err != nil { - return err - } nodeMap, err := apply.ParseNodes(nodes) if err != nil { @@ -119,7 +120,6 @@ func newRunCmd() *cobra.Command { }, } runCmd.Flags().StringVar(&vm.Spec.SSH.PkPasswd, "pk-passwd", "", "passphrase for decrypting a PEM encoded private key") - runCmd.Flags().StringVarP(&vm.Spec.Type, "type", "t", v1.MultipassType, "choose a type of infra, multipass") runCmd.Flags().StringVar(&vm.Name, "name", "default", "name of cluster to applied init action") runCmd.Flags().StringVarP(&nodes, "nodes", "n", "", "number of nodes, eg: node:1,node2:2") return runCmd diff --git a/docs/examples/action.yaml b/docs/examples/multipass/action.yaml similarity index 100% rename from docs/examples/action.yaml rename to docs/examples/multipass/action.yaml diff --git a/docs/examples/rebuild.yaml b/docs/examples/multipass/rebuild.yaml similarity index 100% rename from docs/examples/rebuild.yaml rename to docs/examples/multipass/rebuild.yaml diff --git a/docs/examples/orb/README.md b/docs/examples/orb/README.md new file mode 100644 index 0000000..df1bb4e --- /dev/null +++ b/docs/examples/orb/README.md @@ -0,0 +1,3 @@ +## orb配置 +需要注意一下,orb不必需要mount和unmount 默认会挂载/Users/cuisongliu目录到Linux服务器,需要调整对应的配置 + diff --git a/docs/examples/orb/action.yaml b/docs/examples/orb/action.yaml new file mode 100644 index 0000000..0c6d3fc --- /dev/null +++ b/docs/examples/orb/action.yaml @@ -0,0 +1,35 @@ +apiVersion: virtual-machine.sealos.io/v1 +kind: Action +spec: + data: + - mount: + source: /Users/cuisongliu/Workspaces/go/src/github.com/labring/sealos + target: /root/go/src/github.com/labring/sealos + - exec: | + sudo apt-get -y update + sudo apt-get install -y make gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu + sudo apt-get install -y ntpdate + sudo apt-get install -y git + sudo ntpdate -s ntp1.aliyun.com + - copyContent: + content: | + #!/bin/bash + version=1.20.1 + arch=arm64 + rm -rf /root/go${version}.linux-${arch}.tar.gz + wget https://studygolang.com/dl/golang/go${version}.linux-${arch}.tar.gz -O /root/go${version}.linux-${arch}.tar.gz + rm -rf /usr/local/go && tar -C /usr/local -zxvf /root/go${version}.linux-${arch}.tar.gz + echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/golang.sh + chmod 0755 /etc/profile.d/golang.sh + rm -rf /root/go${version}.linux-${arch}.tar.gz + mkdir -p /root/go/src/github.com/labring /root/go/bin /root/go/pkg + source /etc/profile.d/golang.sh + go env -w GOPROXY="https://goproxy.io,direct" + target: /root/golang-install.sh + - exec: | + bash /root/golang-install.sh + source /etc/profile.d/golang.sh && echo $PATH + git config --global --add safe.directory /root/go/src/github.com/labring/sealos + cd /Users/cuisongliu/Workspaces/go/src/github.com/labring/sealos && source /etc/profile.d/golang.sh && make build + ons: + - role: master diff --git a/docs/examples/orb/rebuild.yaml b/docs/examples/orb/rebuild.yaml new file mode 100644 index 0000000..d1a9e76 --- /dev/null +++ b/docs/examples/orb/rebuild.yaml @@ -0,0 +1,10 @@ +apiVersion: virtual-machine.sealos.io/v1 +kind: Action +spec: + data: + - exec: | + cd /Users/cuisongliu/Workspaces/go/src/github.com/labring/sealos && source /etc/profile.d/golang.sh && make build + cp bin/linux_arm64/sealos /usr/bin/ + cp bin/linux_arm64/sealctl /usr/bin/ + ons: + - role: master diff --git a/docs/sealvm/sealvm.md b/docs/sealvm/sealvm.md index f586e77..aa82307 100644 --- a/docs/sealvm/sealvm.md +++ b/docs/sealvm/sealvm.md @@ -50,7 +50,7 @@ sealvm action -f action.yaml --debug ## 系统管理命令 -### 安装(install) +### 安装(install) 新版本废弃(v0.2.0) 该命令用于安装虚拟机相关的工具。使用格式如下: diff --git a/go.mod b/go.mod index ff6c296..aaefbf8 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.3.0 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.3 k8s.io/client-go v0.25.0 diff --git a/pkg/actions/runtime/interface.go b/pkg/actions/runtime/interface.go new file mode 100644 index 0000000..344d2ec --- /dev/null +++ b/pkg/actions/runtime/interface.go @@ -0,0 +1,171 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "context" + "fmt" + "github.com/labring/sealvm/pkg/configs" + "github.com/labring/sealvm/pkg/ssh" + "github.com/labring/sealvm/pkg/system" + fileutil "github.com/labring/sealvm/pkg/utils/file" + "github.com/labring/sealvm/pkg/utils/logger" + v1 "github.com/labring/sealvm/types/api/v1" + "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/util/errors" + "os" + "path" +) + +type Interface interface { + MountOnce(name, src, target string) error + UnMountOnce(name, target string) error + Copy(names []string, data v1.ActionData) error + Exec(names []string, data v1.ActionData) error +} + +type action struct { + vm *v1.VirtualMachine + nameAndIp map[string]string + client *ssh.Exec + Interface +} + +func (m *action) Apply(action *v1.Action) error { + action.Status.Phase = v1.ActionPhaseInProcess + var err error + defer func() { + if err != nil { + action.Status.Phase = v1.ActionPhaseFailed + switch err.(type) { + case errors.Aggregate: + action.Status.Message = err.(errors.Aggregate).Error() + default: + action.Status.Message = err.Error() + } + + } + }() + names, nameAndIPs := getNameAndIPs(action, m.vm) + m.nameAndIp = nameAndIPs + if len(names) == 0 { + logger.Warn("lookup names is empty") + return nil + } + logger.Info("lookup names: %v", nameAndIPs) + ips := make([]string, 0) + for _, name := range names { + if _, ok := nameAndIPs[name]; !ok { + return fmt.Errorf("name %s not found", name) + } + ips = append(ips, nameAndIPs[name]) + } + var execClient *ssh.Exec + defaultProvider, _ := system.Get(system.DefaultProvider) + var ii Interface + switch defaultProvider { + case v1.MultipassType: + execClient, err = ssh.NewExecCmdFromIPs(m.vm, ips) + if err != nil { + return err + } + m.client = execClient + ii = newMultiPassAction(m.client) + case v1.OrbType: + ii = newOrbAction() + default: + return fmt.Errorf("action not support type: %s", defaultProvider) + } + m.Interface = ii + fns := []func(names []string, data v1.ActionData) error{ + m.Mount, + m.UnMount, + m.Exec, + m.Copy, + m.CopyContent, + } + for _, data := range action.Spec.Data { + for _, fn := range fns { + fnErr := fn(names, data) + if fnErr != nil { + err = fnErr + return err + } + } + } + action.Status.Phase = v1.ActionPhaseComplete + return nil +} + +func (m *action) Mount(names []string, data v1.ActionData) error { + if data.ActionMount == nil { + return nil + } + if data.ActionMount.Source == "" || data.ActionMount.Target == "" { + return fmt.Errorf("mount data is empty source or target") + } + eg, _ := errgroup.WithContext(context.Background()) + + for _, name := range names { + name := name + eg.Go(func() error { + logger.Debug("mount %s %s:%s", data.ActionMount.Source, name, data.ActionMount.Target) + return m.MountOnce(name, data.ActionMount.Source, data.ActionMount.Target) + }) + } + return eg.Wait() +} +func (m *action) UnMount(names []string, data v1.ActionData) error { + if data.ActionUmount == "" { + return nil + } + eg, _ := errgroup.WithContext(context.Background()) + + for _, name := range names { + name := name + eg.Go(func() error { + logger.Debug("unmount %s:%s", name, data.ActionUmount) + return m.UnMountOnce(name, data.ActionUmount) + }) + } + return eg.Wait() +} + +func (m *action) CopyContent(names []string, data v1.ActionData) error { + if data.ActionCopyContent == nil { + return nil + } + if data.ActionCopyContent.Target == "" { + return fmt.Errorf("copy data is empty target") + } + tmpDir := path.Join(configs.DefaultRootfsDir(), "tmp") + _ = os.MkdirAll(tmpDir, 0755) + newDir, _ := fileutil.MkTmpdir(tmpDir) + defer func() { + _ = os.RemoveAll(newDir) + }() + newFile := path.Join(newDir, "action-generator.sh") + _ = fileutil.WriteFile(newFile, []byte(data.ActionCopyContent.Content)) + logger.Debug("copy content to %s", data.ActionCopyContent.Target) + newData := v1.ActionData{ + ActionCopy: &v1.SourceAndTarget{ + Source: newFile, + Target: data.ActionCopyContent.Target, + }, + } + return m.Copy(names, newData) +} diff --git a/pkg/actions/runtime/mulitipass.go b/pkg/actions/runtime/mulitipass.go index 1851ad6..fc3550c 100644 --- a/pkg/actions/runtime/mulitipass.go +++ b/pkg/actions/runtime/mulitipass.go @@ -17,119 +17,23 @@ limitations under the License. package runtime import ( - "context" "fmt" - "github.com/labring/sealvm/pkg/configs" "github.com/labring/sealvm/pkg/ssh" "github.com/labring/sealvm/pkg/utils/exec" - fileutil "github.com/labring/sealvm/pkg/utils/file" "github.com/labring/sealvm/pkg/utils/logger" v1 "github.com/labring/sealvm/types/api/v1" - "golang.org/x/sync/errgroup" - "k8s.io/apimachinery/pkg/util/errors" - "os" - "path" ) -type multiPassAction struct { - vm *v1.VirtualMachine - nameAndIp map[string]string - client *ssh.Exec -} - -func (m *multiPassAction) Apply(action *v1.Action) error { - action.Status.Phase = v1.ActionPhaseInProcess - var err error - defer func() { - if err != nil { - action.Status.Phase = v1.ActionPhaseFailed - switch err.(type) { - case errors.Aggregate: - action.Status.Message = err.(errors.Aggregate).Error() - default: - action.Status.Message = err.Error() - } - - } - }() - names, nameAndIPs := getNameAndIPs(action, m.vm) - m.nameAndIp = nameAndIPs - if len(names) == 0 { - logger.Warn("lookup names is empty") - return nil - } - logger.Info("lookup names: %v", nameAndIPs) - ips := make([]string, 0) - for _, name := range names { - if _, ok := nameAndIPs[name]; !ok { - return fmt.Errorf("name %s not found", name) - } - ips = append(ips, nameAndIPs[name]) - } - var execClient *ssh.Exec - execClient, err = ssh.NewExecCmdFromIPs(m.vm, ips) - if err != nil { - return err +func newMultiPassAction(client *ssh.Exec) Interface { + return &multiPassAction{ + client: client, } - m.client = execClient - fns := []func(names []string, data v1.ActionData) error{ - m.Mount, - m.UnMount, - m.Exec, - m.Copy, - m.CopyContent, - } - errArr := make([]error, 0) - for _, data := range action.Spec.Data { - for _, fn := range fns { - fnErr := fn(names, data) - if fnErr != nil { - errArr = append(errArr, fnErr) - break - } - } - } - if len(errArr) > 0 { - err = errors.NewAggregate(errArr) - return err - } - action.Status.Phase = v1.ActionPhaseComplete - return nil } -func (m *multiPassAction) Mount(names []string, data v1.ActionData) error { - if data.ActionMount == nil { - return nil - } - if data.ActionMount.Source == "" || data.ActionMount.Target == "" { - return fmt.Errorf("mount data is empty source or target") - } - eg, _ := errgroup.WithContext(context.Background()) - - for _, name := range names { - name := name - eg.Go(func() error { - logger.Debug("mount %s %s:%s", data.ActionMount.Source, name, data.ActionMount.Target) - return m.mount(name, data.ActionMount.Source, data.ActionMount.Target) - }) - } - return eg.Wait() +type multiPassAction struct { + client *ssh.Exec } -func (m *multiPassAction) UnMount(names []string, data v1.ActionData) error { - if data.ActionUmount == "" { - return nil - } - eg, _ := errgroup.WithContext(context.Background()) - for _, name := range names { - name := name - eg.Go(func() error { - logger.Debug("unmount %s:%s", name, data.ActionUmount) - return m.unmount(name, data.ActionUmount) - }) - } - return eg.Wait() -} func (m *multiPassAction) Exec(names []string, data v1.ActionData) error { if data.ActionExec == "" { return nil @@ -147,31 +51,14 @@ func (m *multiPassAction) Copy(names []string, data v1.ActionData) error { logger.Debug("names %+v,copy from %s to %s", names, data.ActionCopy.Source, data.ActionCopy.Target) return m.client.RunCopy(data.ActionCopy.Source, data.ActionCopy.Target) } -func (m *multiPassAction) CopyContent(_ []string, data v1.ActionData) error { - if data.ActionCopyContent == nil { - return nil - } - if data.ActionCopyContent.Target == "" { - return fmt.Errorf("copy data is empty target") - } - tmpDir := path.Join(configs.DefaultRootfsDir(), "tmp") - _ = os.MkdirAll(tmpDir, 0755) - newDir, _ := fileutil.MkTmpdir(tmpDir) - defer func() { - _ = os.RemoveAll(newDir) - }() - newFile := path.Join(newDir, "action-generator.sh") - _ = fileutil.WriteFile(newFile, []byte(data.ActionCopyContent.Content)) - logger.Debug("copy content to %s", data.ActionCopyContent.Target) - return m.client.RunCopy(newFile, data.ActionCopyContent.Target) -} -func (m *multiPassAction) mount(name, src, target string) error { + +func (m *multiPassAction) MountOnce(name, src, target string) error { cmd := fmt.Sprintf("multipass mount %s %s:%s", src, name, target) logger.Info("executing... %s \n", cmd) return exec.Cmd("bash", "-c", cmd) } -func (m *multiPassAction) unmount(name, target string) error { +func (m *multiPassAction) UnMountOnce(name, target string) error { cmd := fmt.Sprintf("multipass unmount %s:%s", name, target) logger.Info("executing... %s \n", cmd) return exec.Cmd("bash", "-c", cmd) diff --git a/pkg/actions/runtime/orb.go b/pkg/actions/runtime/orb.go new file mode 100644 index 0000000..3189571 --- /dev/null +++ b/pkg/actions/runtime/orb.go @@ -0,0 +1,90 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "context" + "fmt" + "github.com/labring/sealvm/pkg/utils/exec" + "github.com/labring/sealvm/pkg/utils/logger" + v1 "github.com/labring/sealvm/types/api/v1" + "golang.org/x/sync/errgroup" + "strings" +) + +func newOrbAction() Interface { + return &orbAction{} +} + +type orbAction struct { + multiPassAction +} + +func (m *orbAction) MountOnce(name, src, target string) error { + logger.Warn("orb does not need to support mount, it is mounted in the root directory by default") + return nil +} + +func (m *orbAction) UnMountOnce(name, target string) error { + logger.Warn("orb does not need to support mount, it is mounted in the root directory by default") + return nil +} + +func (m *orbAction) Exec(names []string, data v1.ActionData) error { + if data.ActionExec == "" { + return nil + } + logger.Debug("names %+v,exec %s", names, data.ActionExec) + + for _, name := range names { + for _, cmd := range strings.Split(data.ActionExec, "\n") { + if strings.TrimSpace(cmd) == "" { + continue + } + err := exec.Cmd("/bin/bash", "-c", fmt.Sprintf("ssh root@%s@orb \"%s\"", name, cmd)) + if err != nil { + return err + } + } + + } + return nil +} +func (m *orbAction) Copy(names []string, data v1.ActionData) error { + if data.ActionCopy == nil { + return nil + } + if data.ActionCopy.Source == "" || data.ActionCopy.Target == "" { + return fmt.Errorf("copy data is empty source or target") + } + logger.Debug("names %+v,copy from %s to %s", names, data.ActionCopy.Source, data.ActionCopy.Target) + eg, _ := errgroup.WithContext(context.Background()) + for _, name := range names { + name := name + eg.Go(func() error { + err := exec.Cmd("/bin/bash", "-c", fmt.Sprintf("scp %s root@%s@orb:%s", data.ActionCopy.Source, name, data.ActionCopy.Target)) + if err != nil { + return err + } + return nil + }) + } + if err := eg.Wait(); err != nil { + return fmt.Errorf("failed to exec command, err: %v", err) + } + return nil +} diff --git a/pkg/actions/runtime/runtime.go b/pkg/actions/runtime/runtime.go index c8bb9b1..8cf7806 100644 --- a/pkg/actions/runtime/runtime.go +++ b/pkg/actions/runtime/runtime.go @@ -25,10 +25,6 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ) -type Runtime interface { - Apply(action *v1.Action) error -} - func getNameAndIPs(action *v1.Action, vm *v1.VirtualMachine) ([]string, map[string]string) { if action == nil { return nil, nil @@ -69,19 +65,14 @@ func getNameAndIPs(action *v1.Action, vm *v1.VirtualMachine) ([]string, map[stri return names.List(), data } -func NewAction(name string) (Runtime, error) { +func NewAction(name string) (*action, error) { i, err := process.NewInterfaceFromName(name) if err != nil { logger.Error(err) return nil, err } if i.VMInfo() != nil { - switch i.VMInfo().Spec.Type { - case v1.MultipassType: - return &multiPassAction{vm: i.VMInfo()}, nil - default: - return nil, errors.New("action not support type:" + i.VMInfo().Spec.Type) - } + return &action{vm: i.VMInfo()}, nil } return nil, errors.New("load vm config error") } diff --git a/pkg/apply/args.go b/pkg/apply/args.go index 6d286ac..3b396a7 100644 --- a/pkg/apply/args.go +++ b/pkg/apply/args.go @@ -19,7 +19,6 @@ package apply import ( "errors" "fmt" - "github.com/labring/sealvm/pkg/system" "github.com/labring/sealvm/pkg/template" "github.com/labring/sealvm/pkg/utils/logger" v1 "github.com/labring/sealvm/types/api/v1" @@ -27,14 +26,6 @@ import ( "strings" ) -func GetDefaultImage() (string, error) { - defaultImageLocal, _ := system.Get(system.DefaultImageKey) - if defaultImageLocal != "" { - return defaultImageLocal, nil - } - return "", nil -} - func ValidateTemplate(vm *v1.VirtualMachine) error { if vm.Spec.SSH.PublicFile == "" { return fmt.Errorf("public key is required,please set values using 'sealvm values set'") diff --git a/pkg/apply/infra/infra.go b/pkg/apply/infra/infra.go index 58b814b..59740b0 100644 --- a/pkg/apply/infra/infra.go +++ b/pkg/apply/infra/infra.go @@ -17,20 +17,18 @@ limitations under the License. package infra import ( + "errors" "fmt" - - "github.com/labring/sealvm/pkg/apply/infra/mulitipass" + "github.com/labring/sealvm/pkg/apply/infra/vm" "github.com/labring/sealvm/pkg/apply/runtime" "github.com/labring/sealvm/pkg/configs" + "github.com/labring/sealvm/pkg/system" "github.com/labring/sealvm/pkg/utils/logger" v1 "github.com/labring/sealvm/types/api/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func NewDefaultVirtualMachine(infra *v1.VirtualMachine, cf configs.Interface) (runtime.Interface, error) { - if infra.Spec.Type != v1.MultipassType { - return nil, fmt.Errorf("infra type %s is not supported", infra.Spec.Type) - } if !infra.DeletionTimestamp.IsZero() && infra.CreationTimestamp.IsZero() { logger.Debug("fix VirtualMachine creationTimestamp") t := metav1.Now() @@ -46,14 +44,23 @@ func NewDefaultVirtualMachine(infra *v1.VirtualMachine, cf configs.Interface) (r if !infra.CreationTimestamp.IsZero() && err != nil { return nil, err } - return newMultiPassVirtualMachine(infra, cf) + return newVirtualMachine(infra, cf) } -func newMultiPassVirtualMachine(infra *v1.VirtualMachine, cf configs.Interface) (runtime.Interface, error) { - dr := &mulitipass.MultiPassVirtualMachine{ +func newVirtualMachine(infra *v1.VirtualMachine, cf configs.Interface) (runtime.Interface, error) { + dr := &vm.VirtualMachine{ Desired: infra, Current: cf.GetVirtualMachine(), Config: cf, } + defaultProvider, _ := system.Get(system.DefaultProvider) + switch defaultProvider { + case v1.MultipassType: + dr.Interface = vm.NewMultipass() + case v1.OrbType: + dr.Interface = vm.NewOrb() + default: + return nil, errors.New("infra vm not support type:" + defaultProvider) + } return &driver{Infra: dr}, nil } diff --git a/pkg/apply/infra/vm/cloud_init.go b/pkg/apply/infra/vm/cloud_init.go new file mode 100644 index 0000000..66960e2 --- /dev/null +++ b/pkg/apply/infra/vm/cloud_init.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "fmt" + "github.com/labring/sealvm/pkg/utils/logger" + "gopkg.in/yaml.v3" + "os" + "strings" +) + +type File struct { + Content string `yaml:"content"` + Path string `yaml:"path"` + Permissions string `yaml:"permissions"` +} + +type Config struct { + WriteFiles []File `yaml:"write_files"` + RunCmd []string `yaml:"runcmd"` +} + +func cloudInit(fileName string) *Config { + yamlFile, err := os.ReadFile(fileName) + if err != nil { + logger.Error("yamlFile.Get err #%v ", err) + return nil + } + var config Config + + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + logger.Error("Unmarshal cloud init config: %v", err) + return nil + } + return &config +} + +func (c *Config) toScript() string { + sb := strings.Builder{} + sb.WriteString("#!/bin/bash\n") + for _, f := range c.WriteFiles { + sb.WriteString("cat < ") + sb.WriteString(f.Path) + sb.WriteString("\n") + sb.WriteString(f.Content) + sb.WriteString("\nEOF\n") + sb.WriteString(fmt.Sprintf("chmod %s %s\n", f.Permissions, f.Path)) + } + sb.WriteString("apt-get update -y\n") + sb.WriteString("apt-get install -y openssh-client\n") + sb.WriteString("apt-get install -y openssh-server\n") + sb.WriteString("mkdir -p ~/.ssh\n") + for _, cmd := range c.RunCmd { + if strings.Contains(cmd, "/etc/cloud") { + continue + } + if strings.Contains(cmd, "/var/lib/cloud") { + continue + } + sb.WriteString(cmd) + sb.WriteString("\n") + } + return sb.String() +} diff --git a/pkg/install/install.go b/pkg/apply/infra/vm/cloud_init_test.go similarity index 63% rename from pkg/install/install.go rename to pkg/apply/infra/vm/cloud_init_test.go index ee0d176..0042a3f 100644 --- a/pkg/install/install.go +++ b/pkg/apply/infra/vm/cloud_init_test.go @@ -1,5 +1,5 @@ /* -Copyright 2022 cuisongliu@qq.com. +Copyright 2023 cuisongliu@qq.com. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package install +package vm -import v1 "github.com/labring/sealvm/types/api/v1" +import ( + "testing" +) -var AutoDownload = true - -type Interface interface { - Install() error - IsInstall() bool -} - -func NewInstaller(vmType string) Interface { - switch vmType { - case v1.MultipassType: - return &multipass{} +func Test_cloudInit(t *testing.T) { + cfg := cloudInit("/Users/cuisongliu/.sealvm/etc/default/node.yaml") + if cfg == nil { + t.Error("cloudInit error") + return } - return nil + t.Log(cfg.toScript()) } diff --git a/pkg/apply/infra/mulitipass/init.go b/pkg/apply/infra/vm/init.go similarity index 74% rename from pkg/apply/infra/mulitipass/init.go rename to pkg/apply/infra/vm/init.go index 2c20b6e..8c9bd5b 100644 --- a/pkg/apply/infra/mulitipass/init.go +++ b/pkg/apply/infra/vm/init.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package mulitipass +package vm import ( "context" @@ -24,9 +24,7 @@ import ( "time" "github.com/labring/sealvm/pkg/configs" - "github.com/labring/sealvm/pkg/ssh" "github.com/labring/sealvm/pkg/template" - "github.com/labring/sealvm/pkg/utils/exec" fileutil "github.com/labring/sealvm/pkg/utils/file" "github.com/labring/sealvm/pkg/utils/logger" "github.com/labring/sealvm/pkg/utils/strings" @@ -37,15 +35,15 @@ import ( "k8s.io/client-go/util/retry" ) -func (r *MultiPassVirtualMachine) DesiredVM() *v1.VirtualMachine { +func (r *VirtualMachine) DesiredVM() *v1.VirtualMachine { return r.Desired } -func (r *MultiPassVirtualMachine) CurrentVM() *v1.VirtualMachine { +func (r *VirtualMachine) CurrentVM() *v1.VirtualMachine { return r.Current } -func (r *MultiPassVirtualMachine) Init() { +func (r *VirtualMachine) Init() { logger.Info("Start to create a new infra:", r.Desired.Name) pipelines := []func(infra *v1.VirtualMachine){ @@ -72,7 +70,7 @@ func GetCloudInitYamlByRole(clusterName, role string) string { return path.Join(configs.GetEtcDir(clusterName), fmt.Sprintf("%s.yaml", role)) } -func (r *MultiPassVirtualMachine) InitStatus(infra *v1.VirtualMachine) { +func (r *VirtualMachine) InitStatus(infra *v1.VirtualMachine) { logger.Info("Start to exec InitStatus:", r.Desired.Name) var initializedCondition = &v1.Condition{ Type: "Initialized", @@ -85,13 +83,13 @@ func (r *MultiPassVirtualMachine) InitStatus(infra *v1.VirtualMachine) { infra.Status.Phase = v1.PhaseInProcess } -func (r *MultiPassVirtualMachine) ApplyConfig(infra *v1.VirtualMachine) { +func (r *VirtualMachine) ApplyConfig(infra *v1.VirtualMachine) { logger.Info("Start to exec ApplyConfig:", r.Desired.Name) var configCondition = &v1.Condition{ Type: "Config", Status: v12.ConditionTrue, Reason: "Config Generated", - Message: "config has been generated to launch multipass", + Message: "config has been generated to launch local vm", LastHeartbeatTime: metav1.Now(), } defer r.saveCondition(infra, configCondition) @@ -107,13 +105,13 @@ func (r *MultiPassVirtualMachine) ApplyConfig(infra *v1.VirtualMachine) { } } -func (r *MultiPassVirtualMachine) CreateVMs(infra *v1.VirtualMachine) { +func (r *VirtualMachine) CreateVMs(infra *v1.VirtualMachine) { logger.Info("Start to exec CreateVMs:", r.Desired.Name) var configCondition = &v1.Condition{ Type: "InitVMs", Status: v12.ConditionTrue, Reason: "VM start", - Message: "launch multipass success", + Message: "launch local vm success", LastHeartbeatTime: metav1.Now(), } defer r.saveCondition(infra, configCondition) @@ -144,13 +142,13 @@ func (r *MultiPassVirtualMachine) CreateVMs(infra *v1.VirtualMachine) { } } -func (r *MultiPassVirtualMachine) SyncVMs(infra *v1.VirtualMachine) { +func (r *VirtualMachine) SyncVMs(infra *v1.VirtualMachine) { logger.Info("Start to exec SyncVMs:", r.Desired.Name) var configCondition = &v1.Condition{ Type: "SyncVMs", Status: v12.ConditionTrue, Reason: "VM status sync", - Message: "multipass instance sync success", + Message: "local vm instance sync success", LastHeartbeatTime: metav1.Now(), } defer r.saveCondition(infra, configCondition) @@ -170,7 +168,7 @@ func (r *MultiPassVirtualMachine) SyncVMs(infra *v1.VirtualMachine) { } info = newInfo } - if info.State != "Running" { + if !info.IsRunning() { return fmt.Errorf("instance %s is not running", infra.Name) } return nil @@ -185,7 +183,7 @@ func (r *MultiPassVirtualMachine) SyncVMs(infra *v1.VirtualMachine) { infra.Status.Hosts = status } -func (r *MultiPassVirtualMachine) PingVms(infra *v1.VirtualMachine) { +func (r *VirtualMachine) PingVms(infra *v1.VirtualMachine) { if !v1.IsConditionsTrue(infra.Status.Conditions) { logger.Info("Skip to exec PingVms:", r.Desired.Name) return @@ -195,48 +193,32 @@ func (r *MultiPassVirtualMachine) PingVms(infra *v1.VirtualMachine) { Type: "PingVms", Status: v12.ConditionTrue, Reason: "VM ssh ping", - Message: "multipass instance ssh ping success", + Message: "local vm instance ssh ping success", LastHeartbeatTime: metav1.Now(), } defer r.saveCondition(infra, configCondition) - client := ssh.NewSSHClient(&infra.Spec.SSH, true) - var ips []string + var ips []v1.VirtualMachineHostStatus for _, host := range infra.Status.Hosts { - if host.State != "Running" { + if !host.IsRunning() { v1.SetConditionError(configCondition, "VMStatus", fmt.Errorf("vm status is not running")) continue } - ips = append(ips, host.IPs[0]) + ips = append(ips, host) } - err := ssh.WaitSSHReady(client, 6, ips...) + err := r.PingVmsForHosts(infra, ips) if err != nil { logger.Error("ping vms is error: %+v", err) return } } -func (r *MultiPassVirtualMachine) CreateVM(infra *v1.VirtualMachine, host *v1.Host, index int) error { - cfg := GetCloudInitYamlByRole(infra.Name, host.Role) - debugFlag := "" - if logger.IsDebugMode() { - debugFlag = "-vvv" - } - vmID := strings.GetID(infra.Name, host.Role, index) - if _, err := r.GetById(vmID); err != nil { - cmd := fmt.Sprintf("multipass launch --name %s --cpus %s --mem %sG --disk %sG --cloud-init %s %s %s ", strings.GetID(infra.Name, host.Role, index), host.Resources[v1.CPUKey], host.Resources[v1.MEMKey], host.Resources[v1.DISKKey], cfg, debugFlag, host.Image) - logger.Info("executing... %s \n", cmd) - return exec.Cmd("bash", "-c", cmd) - } - return nil -} - -func (r *MultiPassVirtualMachine) FinalStatus(infra *v1.VirtualMachine) { +func (r *VirtualMachine) FinalStatus(infra *v1.VirtualMachine) { condition := &v1.Condition{ Type: "Ready", Status: v12.ConditionTrue, LastHeartbeatTime: metav1.Now(), Reason: "Ready", - Message: "MultiPass is available now", + Message: "local vm is available now", } defer r.saveCondition(infra, condition) @@ -244,7 +226,7 @@ func (r *MultiPassVirtualMachine) FinalStatus(infra *v1.VirtualMachine) { condition.LastHeartbeatTime = metav1.Now() condition.Status = v12.ConditionFalse condition.Reason = "NotReady" - condition.Message = "MultiPass is not available now" + condition.Message = "local vm is not available now" infra.Status.Phase = v1.PhaseFailed } else { infra.Status.Phase = v1.PhaseSuccess @@ -252,7 +234,7 @@ func (r *MultiPassVirtualMachine) FinalStatus(infra *v1.VirtualMachine) { } // Language: go -func (r *MultiPassVirtualMachine) saveCondition(infra *v1.VirtualMachine, condition *v1.Condition) { +func (r *VirtualMachine) saveCondition(infra *v1.VirtualMachine, condition *v1.Condition) { if !v1.IsConditionTrue(infra.Status.Conditions, *condition) { infra.Status.Conditions = v1.UpdateCondition(infra.Status.Conditions, *condition) } diff --git a/pkg/apply/infra/mulitipass/reconcile.go b/pkg/apply/infra/vm/multipass.go similarity index 57% rename from pkg/apply/infra/mulitipass/reconcile.go rename to pkg/apply/infra/vm/multipass.go index 00f79e7..f5b30a6 100644 --- a/pkg/apply/infra/mulitipass/reconcile.go +++ b/pkg/apply/infra/vm/multipass.go @@ -1,5 +1,5 @@ /* -Copyright 2022 cuisongliu@qq.com. +Copyright 2023 cuisongliu@qq.com. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,135 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -package mulitipass +package vm import ( - "context" "errors" "fmt" - "strconv" - strings2 "strings" - "time" - "github.com/dustin/go-humanize" - "github.com/labring/sealvm/pkg/apply/runtime" + "github.com/labring/sealvm/pkg/ssh" "github.com/labring/sealvm/pkg/utils/exec" "github.com/labring/sealvm/pkg/utils/logger" "github.com/labring/sealvm/pkg/utils/strings" v1 "github.com/labring/sealvm/types/api/v1" errors2 "github.com/pkg/errors" - "golang.org/x/sync/errgroup" - v12 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/json" + "strconv" + strings2 "strings" ) -func (r *MultiPassVirtualMachine) Reconcile(diff runtime.Diff) { - logger.Info("Start to reconcile a new infra:", r.Desired.Name) - r.DiffFunc = diff - pipelines := []func(infra *v1.VirtualMachine){ - r.InitStatus, - r.ApplyConfig, - r.ApplyVMs, - r.SyncVMs, - r.PingVms, - r.FinalStatus, - } - if !r.Desired.DeletionTimestamp.IsZero() { - pipelines = []func(infra *v1.VirtualMachine){ - r.InitStatus, - r.DeleteVMs, - } - } - for _, fn := range pipelines { - fn(r.Desired) - } - if r.Desired.Status.Phase != v1.PhaseFailed { - r.Desired.Status.Phase = v1.PhaseSuccess - } - logger.Info("succeeded in reconcile, enjoy it!") -} -func (r *MultiPassVirtualMachine) ApplyVMs(infra *v1.VirtualMachine) { - logger.Info("Start to exec ApplyVMs:", r.Desired.Name) - var configCondition = &v1.Condition{ - Type: "ApplyVMs", - Status: v12.ConditionTrue, - Reason: "VM apply", - Message: "apply multipass success", - LastHeartbeatTime: metav1.Now(), - } - defer r.saveCondition(infra, configCondition) - addHostNames, deleteHostNames := r.DiffFunc(r.Current, r.Desired) - - eg, _ := errgroup.WithContext(context.Background()) - - for _, host := range addHostNames { - h := host - eg.Go(func() error { - _, role, index := strings.GetHostV1FromAliasName(h) - hostObj := infra.GetHostByRole(role) - if hostObj != nil { - indexInt, err := strconv.Atoi(index) - if err == nil { - time.Sleep(time.Duration(indexInt) * time.Millisecond * 100) - return r.CreateVM(infra, hostObj, indexInt) - } - return err - } - return fmt.Errorf("not found host from role: %s", role) - }) - } - - for _, host := range deleteHostNames { - h := host - eg.Go(func() error { - _, role, index := strings.GetHostV1FromAliasName(h) - indexInt, err := strconv.Atoi(index) - if err == nil { - hostStatus := r.Current.GetHostStatusByRoleIndex(role, indexInt) - if hostStatus != nil { - return r.DeleteVM(r.Current, hostStatus) - } - return fmt.Errorf("not found host status from role: %s, index: %d", role, indexInt) - } - return err - }) - } - - if err := eg.Wait(); err != nil { - v1.SetConditionError(configCondition, "ApplyVMsError", err) - return - } +func NewMultipass() Interface { + return &multipass{} } -func (r *MultiPassVirtualMachine) DeleteVMs(infra *v1.VirtualMachine) { - logger.Info("Start to exec DeleteVMs:", r.Desired.Name) - var configCondition = &v1.Condition{ - Type: "DeleteVMs", - Status: v12.ConditionTrue, - Reason: "Delete VMs", - Message: "config has been delete multipass instances", - LastHeartbeatTime: metav1.Now(), - } - defer r.saveCondition(infra, configCondition) - - eg, _ := errgroup.WithContext(context.Background()) - - for _, host := range infra.Status.Hosts { - dHost := host - eg.Go(func() error { - return r.DeleteVM(infra, &dHost) - }) - } - if err := eg.Wait(); err != nil { - v1.SetConditionError(configCondition, "DeleteVMsError", err) - return - } - +type multipass struct { } -func (r *MultiPassVirtualMachine) DeleteVM(infra *v1.VirtualMachine, host *v1.VirtualMachineHostStatus) error { +func (r *multipass) DeleteVM(infra *v1.VirtualMachine, host *v1.VirtualMachineHostStatus) error { if _, err := r.GetById(host.ID); err == nil { cmd := fmt.Sprintf("multipass stop %s && multipass delete -p %s ", host.ID, host.ID) return exec.Cmd("bash", "-c", cmd) @@ -150,7 +47,7 @@ func (r *MultiPassVirtualMachine) DeleteVM(infra *v1.VirtualMachine, host *v1.Vi return nil } -func (r *MultiPassVirtualMachine) Get(name, role string, index int) (string, error) { +func (r *multipass) Get(name, role string, index int) (string, error) { cmd := fmt.Sprintf("multipass info %s --format=json", strings.GetID(name, role, index)) out, _ := exec.RunBashCmd(cmd) if out == "" { @@ -159,7 +56,7 @@ func (r *MultiPassVirtualMachine) Get(name, role string, index int) (string, err return out, nil } -func (r *MultiPassVirtualMachine) List() (string, error) { +func (r *multipass) List() (string, error) { cmd := fmt.Sprintf("multipass list --format json") out, _ := exec.RunBashCmd(cmd) if out == "" { @@ -167,8 +64,57 @@ func (r *MultiPassVirtualMachine) List() (string, error) { } return out, nil } +func (r *multipass) InspectByList(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { + type ListData struct { + List []struct { + Ipv4 []string `json:"ipv4"` + Name string `json:"name"` + Release string `json:"release"` + State string `json:"state"` + } `json:"list"` + } + + data, err := r.List() + if err != nil { + return nil, err + } + var outStruct ListData + err = json.Unmarshal([]byte(data), &outStruct) + if err != nil { + return nil, errors2.Wrap(err, "decode out json from local vm info failed") + } + + for _, l := range outStruct.List { + if l.Name == strings.GetID(name, role.Role, index) { + newIPs := make([]string, 0) + if len(l.Ipv4) > 0 { + for _, ip := range l.Ipv4 { + if strings2.HasPrefix(ip, "172.17") || strings2.HasPrefix(ip, "10.96") { + continue + } else { + newIPs = append(newIPs, ip) + } + } + } + + return &v1.VirtualMachineHostStatus{ + State: l.State, + Role: role.Role, + ID: strings.GetID(name, role.Role, index), + IPs: newIPs, + ImageID: "", + ImageName: l.Release, + Capacity: nil, + Used: map[string]string{}, + Mounts: map[string]string{}, + Index: index, + }, nil + } + } + return nil, errors.New("not found this instance") +} -func (r *MultiPassVirtualMachine) GetById(name string) (string, error) { +func (r *multipass) GetById(name string) (string, error) { cmd := fmt.Sprintf("multipass info %s --format=json", name) out, _ := exec.RunBashCmd(cmd) if out == "" || strings2.Contains(out, "does not exist") { @@ -176,8 +122,22 @@ func (r *MultiPassVirtualMachine) GetById(name string) (string, error) { } return out, nil } +func (r *multipass) CreateVM(infra *v1.VirtualMachine, host *v1.Host, index int) error { + cfg := GetCloudInitYamlByRole(infra.Name, host.Role) + debugFlag := "" + if logger.IsDebugMode() { + debugFlag = "-vvv" + } + vmID := strings.GetID(infra.Name, host.Role, index) + if _, err := r.GetById(vmID); err != nil { + cmd := fmt.Sprintf("multipass launch --name %s --cpus %s --mem %sG --disk %sG --cloud-init %s %s %s ", strings.GetID(infra.Name, host.Role, index), host.Resources[v1.CPUKey], host.Resources[v1.MEMKey], host.Resources[v1.DISKKey], cfg, debugFlag, host.Image) + logger.Info("executing... %s \n", cmd) + return exec.Cmd("bash", "-c", cmd) + } + return nil +} -func (r *MultiPassVirtualMachine) Inspect(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { +func (r *multipass) Inspect(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { info, err := r.Get(name, role.Role, index) if err != nil { return nil, err @@ -232,53 +192,15 @@ func (r *MultiPassVirtualMachine) Inspect(name string, role v1.Host, index int) return hostStatus, nil } - -func (r *MultiPassVirtualMachine) InspectByList(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { - type ListData struct { - List []struct { - Ipv4 []string `json:"ipv4"` - Name string `json:"name"` - Release string `json:"release"` - State string `json:"state"` - } `json:"list"` +func (r *multipass) PingVmsForHosts(infra *v1.VirtualMachine, hosts []v1.VirtualMachineHostStatus) error { + client := ssh.NewSSHClient(&infra.Spec.SSH, true) + var ips []string + for _, host := range hosts { + ips = append(ips, host.IPs[0]) } - - data, err := r.List() + err := ssh.WaitSSHReady(client, 6, ips...) if err != nil { - return nil, err + return err } - var outStruct ListData - err = json.Unmarshal([]byte(data), &outStruct) - if err != nil { - return nil, errors2.Wrap(err, "decode out json from multipass info failed") - } - - for _, l := range outStruct.List { - if l.Name == strings.GetID(name, role.Role, index) { - newIPs := make([]string, 0) - if len(l.Ipv4) > 0 { - for _, ip := range l.Ipv4 { - if strings2.HasPrefix(ip, "172.17") || strings2.HasPrefix(ip, "10.96") { - continue - } else { - newIPs = append(newIPs, ip) - } - } - } - - return &v1.VirtualMachineHostStatus{ - State: l.State, - Role: role.Role, - ID: strings.GetID(name, role.Role, index), - IPs: newIPs, - ImageID: "", - ImageName: l.Release, - Capacity: nil, - Used: map[string]string{}, - Mounts: map[string]string{}, - Index: index, - }, nil - } - } - return nil, errors.New("not found this instance") + return nil } diff --git a/pkg/apply/infra/vm/orb.go b/pkg/apply/infra/vm/orb.go new file mode 100644 index 0000000..c3f06c7 --- /dev/null +++ b/pkg/apply/infra/vm/orb.go @@ -0,0 +1,231 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "errors" + "fmt" + "github.com/labring/sealvm/pkg/configs" + "github.com/labring/sealvm/pkg/utils/exec" + "github.com/labring/sealvm/pkg/utils/logger" + "github.com/labring/sealvm/pkg/utils/strings" + v1 "github.com/labring/sealvm/types/api/v1" + errors2 "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/json" + "os" + "path" + strings2 "strings" +) + +func NewOrb() Interface { + return &orb{} +} + +type orb struct { +} + +func (r *orb) CreateVM(infra *v1.VirtualMachine, host *v1.Host, index int) error { + cfg := GetCloudInitYamlByRole(infra.Name, host.Role) + cloudCfg := cloudInit(cfg) + scriptPath := path.Join(configs.GetEtcDir(infra.Name), fmt.Sprintf("%s.sh", host.Role)) + _ = os.WriteFile(scriptPath, []byte(cloudCfg.toScript()), 0755) + logger.Info("cloud init to bash success") + vmID := strings.GetID(infra.Name, host.Role, index) + if _, err := r.GetById(vmID); err != nil { + //orb create %[1]s %[2]s && orb -m %[2]s -u root %[3]s + cmd := fmt.Sprintf("orb create %[1]s %[2]s && orb -m %[2]s -u root %[3]s", host.Image, strings.GetID(infra.Name, host.Role, index), scriptPath) + logger.Info("executing... %s \n", cmd) + return exec.Cmd("bash", "-c", cmd) + } + return nil +} + +func (r *orb) DeleteVM(infra *v1.VirtualMachine, host *v1.VirtualMachineHostStatus) error { + if _, err := r.GetById(host.ID); err == nil { + cmd := fmt.Sprintf("orbctl delete -f %s", host.ID) + return exec.Cmd("bash", "-c", cmd) + } + return nil +} + +func (r *orb) Get(name, role string, index int) (string, error) { + cmd := fmt.Sprintf("orb info %s --format json", strings.GetID(name, role, index)) + out, _ := exec.RunBashCmd(cmd) + if out == "" { + return "", errors.New("not found instance") + } + return out, nil +} + +type Image struct { + Distro string `json:"distro"` + Version string `json:"version"` + Arch string `json:"arch"` + Variant string `json:"variant"` +} + +type InspectData struct { + ID string `json:"id"` + Name string `json:"name"` + Image Image `json:"image"` + Isolated bool `json:"isolated"` + Builtin bool `json:"builtin"` + State string `json:"state"` +} + +var imgName = func(i *Image) string { + return fmt.Sprintf("%s:%s %s", i.Distro, i.Version, i.Arch) +} + +func (r *orb) getIPs(name string) ([]string, error) { + ipv4 := fmt.Sprintf(`orb run -m %s ip -4 addr show | grep global | awk '{print $2}'`, name) + ipv6 := fmt.Sprintf(`orb run -m %s ip -6 addr show | grep global | awk '{print $2}'`, name) + ipv4Out, _ := exec.RunBashCmd(ipv4) + ipv6Out, _ := exec.RunBashCmd(ipv6) + if ipv4Out == "" && ipv6Out == "" { + return nil, errors.New("not found ip") + } + ips := make([]string, 0) + if ipv4Out != "" { + ipv4OutArr := strings.SplitRemoveEmpty(ipv4Out, "/") + if len(ipv4OutArr) > 0 { + ipv4Out = ipv4OutArr[0] + } + ips = append(ips, strings.TrimSpaceWS(ipv4Out)) + } + if ipv6Out != "" { + ipv6OutArr := strings.SplitRemoveEmpty(ipv6Out, "/") + if len(ipv6OutArr) > 0 { + ipv6Out = ipv6OutArr[0] + } + ips = append(ips, strings.TrimSpaceWS(ipv6Out)) + } + return ips, nil +} + +func (r *orb) InspectByList(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { + type InspectByList []InspectData + cmd := fmt.Sprintf("orb list --format json") + out, _ := exec.RunBashCmd(cmd) + if out == "" { + return nil, errors.New("not found list instances") + } + var outStruct InspectByList + err := json.Unmarshal([]byte(out), &outStruct) + if err != nil { + return nil, errors2.Wrap(err, "decode out json from local vm info failed") + } + + for _, l := range outStruct { + if l.Name == strings.GetID(name, role.Role, index) { + newIPs := make([]string, 0) + newIPs = append(newIPs, fmt.Sprintf("%s@orb", l.Name)) + ips, _ := r.getIPs(l.Name) + if len(ips) > 0 { + newIPs = append(newIPs, ips...) + } + return &v1.VirtualMachineHostStatus{ + State: l.State, + Role: role.Role, + ID: strings.GetID(name, role.Role, index), + IPs: newIPs, + ImageID: "", + ImageName: imgName(&l.Image), + Capacity: nil, + Used: map[string]string{}, + Mounts: map[string]string{}, + Index: index, + }, nil + } + } + return nil, errors.New("not found this instance") +} + +func (r *orb) GetById(name string) (string, error) { + cmd := fmt.Sprintf("orb info %s --format json", name) + out, _ := exec.RunBashCmd(cmd) + if out == "" || strings2.Contains(out, "machine not found") { + return "", errors.New("not found instance") + } + return out, nil +} + +func (r *orb) Inspect(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) { + // { + // "id": "01H48FEKCCNFHMB5NRKBFAKZ3R", + // "name": "new-ubuntu", + // "image": { + // "distro": "ubuntu", + // "version": "jammy", + // "arch": "arm64", + // "variant": "default" + // }, + // "isolated": false, + // "builtin": false, + // "state": "running" + // } + info, err := r.Get(name, role.Role, index) + if err != nil { + return nil, err + } + var outStruct map[string]interface{} + err = json.Unmarshal([]byte(info), &outStruct) + if err != nil { + return nil, errors2.Wrap(err, "decode out json from orb info failed") + } + hostStatus := &v1.VirtualMachineHostStatus{ + State: "", + Role: role.Role, + ID: strings.GetID(name, role.Role, index), + IPs: nil, + ImageID: "", + ImageName: "", + Capacity: nil, + Used: map[string]string{}, + Mounts: map[string]string{}, + } + + hostStatus.Capacity = role.Resources + hostStatus.State, _, _ = unstructured.NestedString(outStruct, "state") + imageName, _, _ := unstructured.NestedString(outStruct, "image", "distro") + imageVersion, _, _ := unstructured.NestedString(outStruct, "image", "version") + imageArch, _, _ := unstructured.NestedString(outStruct, "image", "arch") + hostStatus.ImageName = fmt.Sprintf("%s:%s %s", imageName, imageVersion, imageArch) + + hostStatus.IPs, _, _ = unstructured.NestedStringSlice(outStruct, "info", hostStatus.ID, "ipv4") + newIPs := make([]string, 0) + newIPs = append(newIPs, fmt.Sprintf("%s@orb", hostStatus.ID)) + ips, _ := r.getIPs(hostStatus.ID) + if len(ips) > 0 { + newIPs = append(newIPs, ips...) + } + hostStatus.IPs = newIPs + hostStatus.Index = index + return hostStatus, nil +} + +func (r *orb) PingVmsForHosts(infra *v1.VirtualMachine, hosts []v1.VirtualMachineHostStatus) error { + for _, host := range hosts { + cmd := fmt.Sprintf(`orb run -m %s ip addr`, host.ID) + out, _ := exec.RunBashCmd(cmd) + if out == "" { + return fmt.Errorf("vm %s is not ready", host.ID) + } + } + return nil +} diff --git a/pkg/apply/infra/vm/reconcile.go b/pkg/apply/infra/vm/reconcile.go new file mode 100644 index 0000000..5a08c7c --- /dev/null +++ b/pkg/apply/infra/vm/reconcile.go @@ -0,0 +1,136 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/labring/sealvm/pkg/apply/runtime" + "github.com/labring/sealvm/pkg/utils/logger" + "github.com/labring/sealvm/pkg/utils/strings" + v1 "github.com/labring/sealvm/types/api/v1" + "golang.org/x/sync/errgroup" + v12 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (r *VirtualMachine) Reconcile(diff runtime.Diff) { + logger.Info("Start to reconcile a new infra:", r.Desired.Name) + r.DiffFunc = diff + pipelines := []func(infra *v1.VirtualMachine){ + r.InitStatus, + r.ApplyConfig, + r.ApplyVMs, + r.SyncVMs, + r.PingVms, + r.FinalStatus, + } + if !r.Desired.DeletionTimestamp.IsZero() { + pipelines = []func(infra *v1.VirtualMachine){ + r.InitStatus, + r.DeleteVMs, + } + } + for _, fn := range pipelines { + fn(r.Desired) + } + if r.Desired.Status.Phase != v1.PhaseFailed { + r.Desired.Status.Phase = v1.PhaseSuccess + } + logger.Info("succeeded in reconcile, enjoy it!") +} +func (r *VirtualMachine) ApplyVMs(infra *v1.VirtualMachine) { + logger.Info("Start to exec ApplyVMs:", r.Desired.Name) + var configCondition = &v1.Condition{ + Type: "ApplyVMs", + Status: v12.ConditionTrue, + Reason: "VM apply", + Message: "apply multipass success", + LastHeartbeatTime: metav1.Now(), + } + defer r.saveCondition(infra, configCondition) + addHostNames, deleteHostNames := r.DiffFunc(r.Current, r.Desired) + + eg, _ := errgroup.WithContext(context.Background()) + + for _, host := range addHostNames { + h := host + eg.Go(func() error { + _, role, index := strings.GetHostV1FromAliasName(h) + hostObj := infra.GetHostByRole(role) + if hostObj != nil { + indexInt, err := strconv.Atoi(index) + if err == nil { + time.Sleep(time.Duration(indexInt) * time.Millisecond * 100) + return r.CreateVM(infra, hostObj, indexInt) + } + return err + } + return fmt.Errorf("not found host from role: %s", role) + }) + } + + for _, host := range deleteHostNames { + h := host + eg.Go(func() error { + _, role, index := strings.GetHostV1FromAliasName(h) + indexInt, err := strconv.Atoi(index) + if err == nil { + hostStatus := r.Current.GetHostStatusByRoleIndex(role, indexInt) + if hostStatus != nil { + return r.DeleteVM(r.Current, hostStatus) + } + return fmt.Errorf("not found host status from role: %s, index: %d", role, indexInt) + } + return err + }) + } + + if err := eg.Wait(); err != nil { + v1.SetConditionError(configCondition, "ApplyVMsError", err) + return + } +} + +func (r *VirtualMachine) DeleteVMs(infra *v1.VirtualMachine) { + logger.Info("Start to exec DeleteVMs:", r.Desired.Name) + var configCondition = &v1.Condition{ + Type: "DeleteVMs", + Status: v12.ConditionTrue, + Reason: "Delete VMs", + Message: "config has been delete local vm instances", + LastHeartbeatTime: metav1.Now(), + } + defer r.saveCondition(infra, configCondition) + + eg, _ := errgroup.WithContext(context.Background()) + + for _, host := range infra.Status.Hosts { + dHost := host + eg.Go(func() error { + return r.DeleteVM(infra, &dHost) + }) + } + if err := eg.Wait(); err != nil { + v1.SetConditionError(configCondition, "DeleteVMsError", err) + return + } + +} diff --git a/pkg/apply/infra/mulitipass/reconcile_test.go b/pkg/apply/infra/vm/reconcile_test.go similarity index 93% rename from pkg/apply/infra/mulitipass/reconcile_test.go rename to pkg/apply/infra/vm/reconcile_test.go index 3933efb..7267472 100644 --- a/pkg/apply/infra/mulitipass/reconcile_test.go +++ b/pkg/apply/infra/vm/reconcile_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package mulitipass +package vm import ( "testing" @@ -23,7 +23,7 @@ import ( ) func TestMultiPassVirtualMachine_Get(t *testing.T) { - //r := &MultiPassVirtualMachine{} + //r := &VirtualMachine{} //_, err := r.Get("aa", "cc", 1) //if err != nil { // t.Errorf(err.Error()) diff --git a/pkg/apply/infra/mulitipass/multipass.go b/pkg/apply/infra/vm/vm.go similarity index 58% rename from pkg/apply/infra/mulitipass/multipass.go rename to pkg/apply/infra/vm/vm.go index 97b5762..f2d9990 100644 --- a/pkg/apply/infra/mulitipass/multipass.go +++ b/pkg/apply/infra/vm/vm.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package mulitipass +package vm import ( "github.com/labring/sealvm/pkg/apply/runtime" @@ -22,9 +22,20 @@ import ( v1 "github.com/labring/sealvm/types/api/v1" ) -type MultiPassVirtualMachine struct { +type VirtualMachine struct { Desired *v1.VirtualMachine Current *v1.VirtualMachine Config configs.Interface DiffFunc runtime.Diff + Interface +} + +type Interface interface { + CreateVM(infra *v1.VirtualMachine, host *v1.Host, index int) error + DeleteVM(infra *v1.VirtualMachine, host *v1.VirtualMachineHostStatus) error + Get(name, role string, index int) (string, error) + InspectByList(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) + GetById(name string) (string, error) + Inspect(name string, role v1.Host, index int) (*v1.VirtualMachineHostStatus, error) + PingVmsForHosts(infra *v1.VirtualMachine, hosts []v1.VirtualMachineHostStatus) error } diff --git a/pkg/install/multipass.go b/pkg/install/multipass.go deleted file mode 100644 index 90fee1b..0000000 --- a/pkg/install/multipass.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package install - -import ( - "path" - "runtime" - - "github.com/labring/sealvm/pkg/configs" - "github.com/labring/sealvm/pkg/utils/exec" - fileutil "github.com/labring/sealvm/pkg/utils/file" - "github.com/labring/sealvm/pkg/utils/logger" - "github.com/labring/sealvm/pkg/utils/progress" -) - -type multipass struct{} - -func (i *multipass) Install() error { - macURL := "https://github.com/canonical/multipass/releases/download/v1.8.1/multipass-1.8.1+mac-Darwin.pkg" - newMacURL := "https://github.com/canonical/multipass/releases/download/v1.8.1/multipass-1.8.1+mac-Darwin.pkg" - winURL := "https://github.com/canonical/multipass/releases/download/v1.8.0/multipass-1.8.0+win-win64.exe" - dirName := path.Join(configs.DefaultRootfsDir(), "multipass") - _ = fileutil.MkDirs(dirName) - if runtime.GOOS == "darwin" { - if "arm64" == runtime.GOARCH { - macURL = newMacURL - } - if !AutoDownload { - logger.Info("please download multipass from %s", macURL) - return nil - } - fileName := path.Join(dirName, "multipass.pkg") - if !fileutil.IsExist(fileName) { - err := progress.Download(macURL, fileName) - if err != nil { - return err - } - logger.Info("your multipass pkg download success") - } - return exec.Cmd("open", fileName) - } - if runtime.GOOS == "windows" { - if !AutoDownload { - logger.Info("please download multipass from %s", winURL) - return nil - } - fileName := path.Join(dirName, "multipass.exe") - if !fileutil.IsExist(fileName) { - err := progress.Download(winURL, fileName) - if err != nil { - return err - } - logger.Info("your multipass exe download success") - } - return exec.Cmd("open", fileName) - } - return nil -} - -func (i *multipass) IsInstall() bool { - _, ok := exec.CheckCmdIsExist("multipass") - return ok -} diff --git a/pkg/process/init.go b/pkg/process/init.go index 821e44d..13d58ac 100644 --- a/pkg/process/init.go +++ b/pkg/process/init.go @@ -17,9 +17,7 @@ limitations under the License. package process import ( - "errors" "github.com/labring/sealvm/pkg/configs" - v1 "github.com/labring/sealvm/types/api/v1" ) func NewInterfaceFromName(name string) (Interface, error) { @@ -29,10 +27,5 @@ func NewInterfaceFromName(name string) (Interface, error) { return nil, err } i := cf.GetVirtualMachine() - switch i.Spec.Type { - case v1.MultipassType: - return &mulitipass{vm: i}, nil - default: - return nil, errors.New("not support type:" + i.Spec.Type) - } + return &defaultProcess{vm: i}, nil } diff --git a/pkg/process/interface.go b/pkg/process/interface.go index 6fd8c55..c85ce2a 100644 --- a/pkg/process/interface.go +++ b/pkg/process/interface.go @@ -25,3 +25,19 @@ type Interface interface { Inspect(name string) VMInfo() *v1.VirtualMachine } + +type defaultProcess struct { + vm *v1.VirtualMachine +} + +func (mp *defaultProcess) List() error { + return printVMs(mp.vm) +} + +func (mp *defaultProcess) Inspect(name string) { + inspectHostname(mp.vm, name) +} + +func (mp *defaultProcess) VMInfo() *v1.VirtualMachine { + return mp.vm +} diff --git a/pkg/process/mulitipass.go b/pkg/process/mulitipass.go deleted file mode 100644 index 580f2f0..0000000 --- a/pkg/process/mulitipass.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package process - -import ( - v1 "github.com/labring/sealvm/types/api/v1" -) - -type mulitipass struct { - vm *v1.VirtualMachine -} - -func (mp *mulitipass) List() error { - return printVMs(mp.vm) -} - -func (mp *mulitipass) Inspect(name string) { - inspectHostname(mp.vm, name) -} - -func (mp *mulitipass) VMInfo() *v1.VirtualMachine { - return mp.vm -} diff --git a/pkg/process/prints.go b/pkg/process/prints.go index d0c2066..5cf7a3c 100644 --- a/pkg/process/prints.go +++ b/pkg/process/prints.go @@ -20,6 +20,7 @@ import ( "github.com/labring/sealvm/pkg/utils/strings" v1 "github.com/labring/sealvm/types/api/v1" "github.com/modood/table" + strings2 "strings" ) func printVMs(vm *v1.VirtualMachine) error { @@ -27,7 +28,7 @@ func printVMs(vm *v1.VirtualMachine) error { Name string State string Role string - Ipv4 []string + Ipv4 string Image string } tables := make([]printTable, 0) @@ -43,7 +44,7 @@ func printVMs(vm *v1.VirtualMachine) error { }) } else { tables = append(tables, printTable{ - Ipv4: status.IPs, + Ipv4: strings2.Join(status.IPs, ","), Name: status.ID, Image: status.ImageName, State: status.State, diff --git a/pkg/system/config.go b/pkg/system/config.go index 4090ca2..d27bba8 100644 --- a/pkg/system/config.go +++ b/pkg/system/config.go @@ -21,11 +21,23 @@ import ( "github.com/labring/sealvm/pkg/configs" "github.com/labring/sealvm/pkg/utils/file" "github.com/labring/sealvm/pkg/utils/yaml" + v1 "github.com/labring/sealvm/types/api/v1" "path" ) type envSystemConfig struct{} +func GetDefaultImage() (string, error) { + defaultProvider, _ := Get(DefaultProvider) + switch defaultProvider { + case v1.MultipassType: + return "release:22.04", nil + case v1.OrbType: + return "ubuntu:jammy", nil + } + return "", nil +} + func Get(key string) (string, error) { return globalConfig.getValue(key) } @@ -91,9 +103,9 @@ var configOptions = []ConfigOption{ DefaultValue: "50", }, { - Key: DefaultImageKey, - Description: "sealvm default image local", - DefaultValue: "", + Key: DefaultProvider, + Description: "sealvm default provider", + DefaultValue: "multipass", }, } @@ -101,7 +113,7 @@ const ( DefaultCPUKey = "default_cpu" DefaultMemKey = "default_mem" DefaultDISKKey = "default_disk" - DefaultImageKey = "default_image" + DefaultProvider = "default_provider" ) var defaultDir = path.Join(configs.DefaultRootfsDir(), "etc") diff --git a/types/api/v1/virtual_machine_types.go b/types/api/v1/virtual_machine_types.go index a483a79..fbdff44 100644 --- a/types/api/v1/virtual_machine_types.go +++ b/types/api/v1/virtual_machine_types.go @@ -21,13 +21,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const MultipassType = "Multipass" +const MultipassType = "multipass" +const OrbType = "orb" // VirtualMachineSpec defines the desired state of VirtualMachine type VirtualMachineSpec struct { Hosts []Host `json:"hosts,omitempty"` SSH SSH `json:"ssh"` - Type string `json:"provider,omitempty"` } type SSH struct { @@ -73,6 +73,16 @@ type Condition struct { Message string `json:"message,omitempty"` } +func (s *VirtualMachineHostStatus) IsRunning() bool { + if s.State == "Running" { + return true + } + if s.State == "running" { + return true + } + return false +} + type VirtualMachineHostStatus struct { State string `json:"state"` Role string `json:"roles"`