├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── add_node.go ├── create.go ├── create │ ├── cluster.go │ ├── network.go │ └── node.go ├── current.go ├── download │ └── download.go ├── kubectl.go ├── list.go ├── list │ ├── printer.go │ └── sorter.go ├── remove.go ├── remove │ ├── cluster.go │ ├── network.go │ └── node.go ├── remove_cluster.go ├── remove_node.go ├── root.go ├── start.go ├── start │ ├── cluster.go │ ├── network.go │ └── node.go ├── status.go ├── status │ ├── cluster.go │ ├── network.go │ ├── node.go │ ├── printer.go │ └── sorter.go ├── stop.go ├── stop │ ├── cluster.go │ ├── network.go │ └── node.go ├── switch.go ├── util.go └── validate │ └── libvirt.go ├── config ├── addons │ ├── addons.go │ ├── dns.go │ ├── flannel.go │ └── proxy.go ├── config.go ├── coreos │ ├── cloudConfig.go │ └── coreos.go ├── kubeconfig │ ├── kubeconfig.go │ ├── types.go │ └── users.go ├── kubernetes.go ├── manifests │ ├── apiServer.go │ ├── controllerManager.go │ ├── metadata.go │ └── scheduler.go └── scripts │ ├── installSocat.go │ ├── loadAddons.go │ └── scripts.go ├── libvirt ├── capabilities.go ├── connection.go ├── domain.go ├── domain_template.go ├── network.go ├── pool.go ├── stream.go ├── util.go └── volume.go ├── libvirtxml ├── capabilities.go ├── capabilitiesGust.go ├── capabilitiesGustArch.go ├── capabilitiesHost.go ├── capabilitiesHostCpu.go ├── document.go ├── domain.go ├── domainChannel.go ├── domainDevices.go ├── domainDisk.go ├── domainDiskDriver.go ├── domainDiskSource.go ├── domainDiskTarget.go ├── domainFilesystem.go ├── domainGraphic.go ├── domainInterface.go ├── domainInterfaceSource.go ├── domainMemory.go ├── domainVcpu.go ├── metadata.go ├── name.go ├── network.go ├── networkBridge.go ├── networkDns.go ├── networkDnsHost.go ├── networkDomain.go ├── networkForward.go ├── networkIp.go ├── node.go ├── storagePool.go ├── storagePoolTarget.go ├── storageVolume.go ├── storageVolumeBackingStore.go ├── storageVolumePermissions.go ├── storageVolumeSize.go ├── storageVolumeTarget.go └── storageVolumeTargetFormat.go ├── main.go ├── repository ├── cluster.go └── clusterRepository.go └── util ├── archive.go ├── certs.go ├── file.go ├── http.go ├── io.go ├── net.go ├── os.go ├── tabwriter.go ├── template.go └── user.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /debug 3 | /kcm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## kcm: Kubernetes cluster manager 2 | 3 | ### Prerequisites 4 | 5 | [libvirt](https://libvirt.org) 6 | 7 | [QEMU](http://www.qemu.org)/[KVM](https://www.linux-kvm.org/page/Main_Page) 8 | 9 | ### Installation 10 | 11 | ``` 12 | go install github.com/bcrusu/kcm 13 | ``` 14 | 15 | ### Usage 16 | 17 | #### Create a cluster: 18 | ``` 19 | kcm create mykube 20 | ``` 21 | Creates a cluster named 'mykube', with 2 minion nodes and one master 22 | 23 | ``` 24 | kcm create mykube --node-count=5 --start 25 | ``` 26 | Creates a cluster with 5 nodes and one master and starts it immediately 27 | 28 | #### Start/stop a cluster: 29 | ``` 30 | kcm start mykube 31 | kcm stop mykube 32 | ``` 33 | 34 | #### Remove a cluster: 35 | ``` 36 | kcm remove cluster mykube 37 | ``` 38 | Removes the cluster named 'mykube' and its artefacts (i.e. libvirt objects and files on disk) 39 | 40 | #### Use kubectl to interact with the cluster: 41 | ``` 42 | kcm ctl get pods 43 | kcm ctl apply -f FILENAME 44 | ... 45 | ``` 46 | The 'ctl' command calls the right version of kubectl binary and sets the "--kubeconfig" argument. It uses the following files: 47 | * kubectl: ~/.kcm/cache/kubernetes/KUBE_VERSION/kubernetes/server/bin/kubectl 48 | * kubeconfig: ~/.kcm/config/CLUSTER_NAME/kubeconfig/kubectl 49 | 50 | #### Get cluster status: 51 | ``` 52 | kcm status 53 | ``` 54 | Outputs information similar to: 55 | ``` 56 | CLUSTER STATUS DNS DOMAIN KUBE VERSION COREOS VERSION 57 | mykube Active mykube.kube 1.7.0-beta.2 stable/1353.8.0 58 | 59 | NETWORK STATUS CIDR DNS SERVER 60 | kcm.mykube Active 10.1.0.0/16 10.1.0.1 61 | 62 | NODE STATUS DNS NAME DNS LOOKUP IP 63 | master Active master.mykube.kube OK 10.1.238.138 64 | node1 Active node1.mykube.kube OK 10.1.199.19 65 | node2 Active node2.mykube.kube OK 10.1.97.155 66 | ``` 67 | 68 | ### Items left to do: 69 | 70 | - [ ] Add Kubernetes Dashboard 71 | - [ ] Support clusters with multiple master nodes (via nginx/HAProxy) 72 | - [ ] More netorking options (e.g. weave, calico, etc.) 73 | - [ ] Allow users to pass configuration settings to newly-created clusters (e.g. all vars with prefix 'KCM_' should be made available to Kubernetes) 74 | 75 | ### Inspiration 76 | 77 | [CCM (Cassandra Cluster Manager)](https://github.com/pcmanus/ccm): A script to easily create and destroy an Apache Cassandra cluster on localhost 78 | 79 | [kube-up (deprecated)](https://github.com/kubernetes/kubernetes/tree/master/cluster) 80 | -------------------------------------------------------------------------------- /cmd/add_node.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/bcrusu/kcm/cmd/create" 7 | "github.com/bcrusu/kcm/cmd/start" 8 | "github.com/bcrusu/kcm/cmd/status" 9 | "github.com/bcrusu/kcm/cmd/validate" 10 | "github.com/bcrusu/kcm/libvirt" 11 | "github.com/bcrusu/kcm/repository" 12 | "github.com/bcrusu/kcm/util" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type addNodeCmdState struct { 18 | ClusterName string 19 | IsMaster bool //TODO: allow only one master atm. 20 | SSHPublicKeyPath string 21 | Start bool 22 | CPUs uint 23 | Memory uint 24 | VolumeCapacity uint 25 | } 26 | 27 | func newAddNodeCmd() *cobra.Command { 28 | var cmd = &cobra.Command{ 29 | Use: "add [NODE_NAME]", 30 | Short: "Add a new node to an existing cluster", 31 | SilenceUsage: true, 32 | } 33 | 34 | state := &addNodeCmdState{} 35 | cmd.PersistentFlags().StringVarP(&state.ClusterName, "cluster", "c", "", "Cluster to add the node to. If not specified, the current cluster will be used") 36 | cmd.PersistentFlags().StringVar(&state.SSHPublicKeyPath, "ssh-public-key", util.GetUserDefaultSSHPublicKeyPath(), "SSH public key to use") 37 | cmd.PersistentFlags().BoolVarP(&state.Start, "start", "s", false, "Start the node immediately if the cluster is running") 38 | cmd.PersistentFlags().UintVar(&state.CPUs, "cpu", 1, "Node allocated CPUs") 39 | cmd.PersistentFlags().UintVar(&state.Memory, "memory", 512, "Node memory (MiB)") 40 | cmd.PersistentFlags().UintVar(&state.VolumeCapacity, "volume", 10, "Node volume capacity (GiB)") 41 | cmd.PersistentFlags().BoolVarP(&state.IsMaster, "master", "m", false, "Adds a master node") 42 | 43 | cmd.RunE = state.runE 44 | return cmd 45 | } 46 | 47 | func (s *addNodeCmdState) runE(cmd *cobra.Command, args []string) error { 48 | if len(args) > 1 { 49 | return errors.New("invalid command arguments") 50 | } 51 | 52 | clusterRepository, err := newClusterRepository() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | cluster, err := getWorkingCluster(clusterRepository, s.ClusterName) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | var nodeName string 63 | if len(args) == 1 { 64 | nodeName = args[0] 65 | if _, ok := cluster.Nodes[nodeName]; ok { 66 | return errors.Errorf("cluster '%s' contains node '%s'", cluster.Name, nodeName) 67 | } 68 | } else { 69 | nodeName = s.nextNodeName(*cluster) 70 | } 71 | 72 | node, err := s.createNodeDefinition(nodeName, *cluster) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | // lightweight validation 78 | if err := node.Validate(); err != nil { 79 | return errors.Wrapf(err, "Validation failed") 80 | } 81 | 82 | sshPublicKey, err := readSSHPublicKey(s.SSHPublicKeyPath) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | connection, err := connectLibvirt() 88 | if err != nil { 89 | return err 90 | } 91 | defer connection.Close() 92 | 93 | // check for libvirt name conflicts 94 | if err := validate.LibvirtNodeObjects(connection, node); err != nil { 95 | return err 96 | } 97 | 98 | //persist cluster definition before creating any artefacts (libvirt objects/files on disk/etc.) 99 | cluster.Nodes[node.Name] = node 100 | if err := clusterRepository.Save(*cluster); err != nil { 101 | return err 102 | } 103 | 104 | clusterConfig, err := getClusterConfig(*cluster) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | if err := create.Node(connection, clusterConfig, node, cluster.Network.Name, sshPublicKey); err != nil { 110 | return err 111 | } 112 | 113 | return s.startNode(connection, *cluster, node) 114 | } 115 | 116 | func (s *addNodeCmdState) createNodeDefinition(name string, cluster repository.Cluster) (repository.Node, error) { 117 | domainName := libvirtDomainName(cluster.Name, name) 118 | dnsName := nodeDNSName(name, cluster.DNSDomain) 119 | 120 | return repository.Node{ 121 | Name: name, 122 | IsMaster: s.IsMaster, 123 | Domain: domainName, 124 | CPUs: s.CPUs, 125 | MemoryMiB: s.Memory, 126 | VolumeCapacityGiB: s.VolumeCapacity, 127 | StoragePool: cluster.StoragePool, 128 | BackingStorageVolume: cluster.BackingStorageVolume, 129 | StorageVolume: libvirtStorageVolumeName(domainName), 130 | DNSName: dnsName, 131 | }, nil 132 | } 133 | 134 | func (s *addNodeCmdState) nextNodeName(cluster repository.Cluster) string { 135 | prefix := NodeNamePrefix 136 | if s.IsMaster { 137 | prefix = MasterNodeNamePrefix 138 | } 139 | 140 | // find the first available name 141 | for i := 1; ; i++ { 142 | name := prefix + strconv.FormatInt(int64(i), 10) 143 | if _, ok := cluster.Nodes[name]; !ok { 144 | return name 145 | } 146 | } 147 | } 148 | 149 | func (s *addNodeCmdState) startNode(connection *libvirt.Connection, cluster repository.Cluster, node repository.Node) error { 150 | if !s.Start { 151 | return nil 152 | } 153 | 154 | // start the node only if the cluster is running 155 | running, err := status.IsClusterActive(connection, cluster) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | if !running { 161 | return nil 162 | } 163 | 164 | return start.Node(connection, node) 165 | } 166 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/bcrusu/kcm/cmd/create" 8 | "github.com/bcrusu/kcm/cmd/download" 9 | "github.com/bcrusu/kcm/cmd/start" 10 | "github.com/bcrusu/kcm/cmd/validate" 11 | "github.com/bcrusu/kcm/repository" 12 | "github.com/bcrusu/kcm/util" 13 | "github.com/golang/glog" 14 | "github.com/pkg/errors" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | const DefaultKubernetesVersion = "1.7.0-beta.2" 19 | const DefaultCoreOSVersion = "1353.8.0" 20 | const DefaultCoreOsChannel = "stable" 21 | const DefaultCNIVersion = "0799f5732f2a11b329d9e3d51b9c8f2e3759f2ff" 22 | 23 | type createCmdState struct { 24 | KubernetesVersion string 25 | CNIVersion string 26 | CoreOSVersion string 27 | CoreOSChannel string 28 | LibvirtStoragePool string 29 | NodesCount uint 30 | KubernetesNetwork string 31 | SSHPublicKeyPath string 32 | Start bool 33 | IPv4CIDR string 34 | MasterCPUs uint 35 | MasterMemory uint 36 | MasterVolumeCapacity uint 37 | NondeCPUs uint 38 | NodeMemory uint 39 | NodeVolumeCapacity uint 40 | } 41 | 42 | func newCreateCmd() *cobra.Command { 43 | var cmd = &cobra.Command{ 44 | Use: "create CLUSTER_NAME", 45 | Short: "Create a new cluster", 46 | SilenceUsage: true, 47 | } 48 | 49 | state := &createCmdState{} 50 | 51 | cmd.PersistentFlags().StringVar(&state.KubernetesVersion, "kubernetes-version", DefaultKubernetesVersion, "Kubernetes version to use") 52 | cmd.PersistentFlags().StringVar(&state.CNIVersion, "cni-version", DefaultCNIVersion, "CNI version to use") 53 | cmd.PersistentFlags().StringVar(&state.CoreOSVersion, "coreos-version", DefaultCoreOSVersion, "CoreOS version to use") 54 | cmd.PersistentFlags().StringVar(&state.CoreOSChannel, "coreos-channel", DefaultCoreOsChannel, "CoreOS release channel: stable, beta, alpha") 55 | cmd.PersistentFlags().StringVar(&state.LibvirtStoragePool, "libvirt-pool", "default", "Libvirt storage pool") 56 | cmd.PersistentFlags().UintVar(&state.NodesCount, "node-count", 2, "Initial number of nodes in the cluster") 57 | cmd.PersistentFlags().StringVar(&state.KubernetesNetwork, "kubernetes-network", "flannel", "Networking mode to use. Only flannel is suppoted at the moment") 58 | cmd.PersistentFlags().StringVar(&state.SSHPublicKeyPath, "ssh-public-key", util.GetUserDefaultSSHPublicKeyPath(), "SSH public key to use") 59 | cmd.PersistentFlags().BoolVarP(&state.Start, "start", "s", false, "Start the cluster immediately") 60 | cmd.PersistentFlags().StringVar(&state.IPv4CIDR, "ipv4-cidr", "10.1.0.0/16", "Libvirt network IPv4 CIDR. Network 10.2.0.0/16 is reserved for pods/services network") 61 | cmd.PersistentFlags().UintVar(&state.MasterCPUs, "master-cpu", 1, "Master node allocated CPUs") 62 | cmd.PersistentFlags().UintVar(&state.MasterVolumeCapacity, "master-volume", 10, "Master volume capacity (GiB)") 63 | cmd.PersistentFlags().UintVar(&state.MasterMemory, "master-memory", 1024, "Master node memory (MiB)") 64 | cmd.PersistentFlags().UintVar(&state.NondeCPUs, "node-cpu", 1, "Node allocated CPUs") 65 | cmd.PersistentFlags().UintVar(&state.NodeMemory, "node-memory", 1024, "Node memory (MiB)") 66 | cmd.PersistentFlags().UintVar(&state.NodeVolumeCapacity, "node-volume", 10, "Node volume capacity (GiB)") 67 | 68 | cmd.RunE = state.runE 69 | return cmd 70 | } 71 | 72 | func (s *createCmdState) runE(cmd *cobra.Command, args []string) error { 73 | if len(args) != 1 { 74 | return errors.New("invalid command arguments") 75 | } 76 | 77 | cluster, err := s.createClusterDefinition(args[0]) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // lightweight validation - valid names, empty strings, no nodes defined, etc. 83 | if err := cluster.Validate(); err != nil { 84 | return errors.Wrapf(err, "Validation failed") 85 | } 86 | 87 | clusterRepository, err := newClusterRepository() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | exists, err := clusterRepository.Exists(cluster.Name) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if exists { 98 | return errors.Errorf("cluster '%s' already exists", cluster.Name) 99 | } 100 | 101 | sshPublicKey, err := readSSHPublicKey(s.SSHPublicKeyPath) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | connection, err := connectLibvirt() 107 | if err != nil { 108 | return err 109 | } 110 | defer connection.Close() 111 | 112 | // check for name conflicts/missing libvirt objects 113 | if err := validate.LibvirtClusterObjects(connection, cluster); err != nil { 114 | return err 115 | } 116 | 117 | if err := download.DownloadPrerequisites(connection, cluster, cacheDir()); err != nil { 118 | return err 119 | } 120 | 121 | //persist cluster definition before creating any artefacts (libvirt/files on disk/etc.) 122 | if err := clusterRepository.Save(cluster); err != nil { 123 | return err 124 | } 125 | 126 | clusterConfig, err := getClusterConfig(cluster) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | if err := create.Cluster(connection, clusterConfig, cluster, sshPublicKey); err != nil { 132 | return err 133 | } 134 | 135 | s.setActiveCluster(clusterRepository, cluster.Name) 136 | 137 | if s.Start { 138 | return start.Cluster(connection, cluster) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (s *createCmdState) createClusterDefinition(clusterName string) (repository.Cluster, error) { 145 | backingStorageVolume := coreOSStorageVolumeName(s.CoreOSVersion) 146 | 147 | caCertificateBytes, caKeyBytes, err := util.CreateCACertificate(clusterName + "-ca") 148 | if err != nil { 149 | return repository.Cluster{}, err 150 | } 151 | 152 | cluster := repository.Cluster{ 153 | Name: clusterName, 154 | KubernetesVersion: s.KubernetesVersion, 155 | CNIVersion: s.CNIVersion, 156 | CoreOSChannel: s.CoreOSChannel, 157 | CoreOSVersion: s.CoreOSVersion, 158 | StoragePool: s.LibvirtStoragePool, 159 | BackingStorageVolume: backingStorageVolume, 160 | Network: repository.Network{ 161 | Name: libvirtNetworkName(clusterName), 162 | IPv4CIDR: s.IPv4CIDR, 163 | }, 164 | Nodes: make(map[string]repository.Node), 165 | CACertificate: caCertificateBytes, 166 | CAPrivateKey: caKeyBytes, 167 | DNSDomain: clusterName + ".kube", 168 | } 169 | 170 | addNode := func(name string, isMaster bool) error { 171 | domainName := libvirtDomainName(clusterName, name) 172 | dnsName := nodeDNSName(name, cluster.DNSDomain) 173 | 174 | node := repository.Node{ 175 | Name: name, 176 | IsMaster: isMaster, 177 | Domain: domainName, 178 | StoragePool: s.LibvirtStoragePool, 179 | BackingStorageVolume: backingStorageVolume, 180 | StorageVolume: libvirtStorageVolumeName(domainName), 181 | DNSName: dnsName, 182 | } 183 | 184 | if isMaster { 185 | node.CPUs = s.MasterCPUs 186 | node.MemoryMiB = s.MasterMemory 187 | node.VolumeCapacityGiB = s.MasterVolumeCapacity 188 | } else { 189 | node.CPUs = s.NondeCPUs 190 | node.MemoryMiB = s.NodeMemory 191 | node.VolumeCapacityGiB = s.NodeVolumeCapacity 192 | } 193 | 194 | cluster.Nodes[name] = node 195 | return nil 196 | } 197 | 198 | masterName := MasterNodeNamePrefix 199 | if err := addNode(masterName, true); err != nil { 200 | return repository.Cluster{}, err 201 | } 202 | 203 | for i := uint(1); i <= s.NodesCount; i++ { 204 | name := NodeNamePrefix + strconv.FormatUint(uint64(i), 10) 205 | if err := addNode(name, false); err != nil { 206 | return repository.Cluster{}, err 207 | } 208 | } 209 | 210 | cluster.ServerURL = fmt.Sprintf("https://%s:6443", cluster.Nodes[masterName].DNSName) 211 | 212 | return cluster, nil 213 | } 214 | 215 | func (s *createCmdState) setActiveCluster(clusterRepository repository.ClusterRepository, name string) { 216 | currentActiveCluster, err := clusterRepository.Current() 217 | if err != nil { 218 | glog.Error(err) 219 | return 220 | } 221 | 222 | if currentActiveCluster != nil { 223 | // only set the active cluster if none is currently set 224 | return 225 | } 226 | 227 | if err := clusterRepository.SetCurrent(name); err != nil { 228 | glog.Errorf("failed to set current cluster. Error: %v", err) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /cmd/create/cluster.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/config" 5 | "github.com/bcrusu/kcm/libvirt" 6 | "github.com/bcrusu/kcm/repository" 7 | ) 8 | 9 | func Cluster(connection *libvirt.Connection, clusterConfig *config.ClusterConfig, 10 | cluster repository.Cluster, sshPublicKey string) error { 11 | if err := clusterConfig.StageCluster(); err != nil { 12 | return err 13 | } 14 | 15 | if err := Network(connection, cluster.Network, cluster.DNSDomain); err != nil { 16 | return err 17 | } 18 | 19 | macAddresses, err := connection.GenerateUniqueMACAddresses(len(cluster.Nodes)) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | index := 0 25 | for _, node := range cluster.Nodes { 26 | if err := nodeInternal(connection, clusterConfig, node, cluster.Network.Name, macAddresses[index], sshPublicKey); err != nil { 27 | return err 28 | } 29 | 30 | index++ 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/create/network.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | func Network(connection *libvirt.Connection, network repository.Network, clusterDomain string) error { 9 | params := libvirt.DefineNetworkParams{ 10 | Name: network.Name, 11 | IPv4CIDR: network.IPv4CIDR, 12 | Domain: clusterDomain, 13 | } 14 | 15 | return connection.DefineNATNetwork(params) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/create/node.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/config" 5 | "github.com/bcrusu/kcm/libvirt" 6 | "github.com/bcrusu/kcm/repository" 7 | ) 8 | 9 | func Node(connection *libvirt.Connection, clusterConfig *config.ClusterConfig, 10 | node repository.Node, networkName, sshPublicKey string) error { 11 | macAddresses, err := connection.GenerateUniqueMACAddresses(1) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | return nodeInternal(connection, clusterConfig, node, networkName, macAddresses[0], sshPublicKey) 17 | } 18 | 19 | func nodeInternal(connection *libvirt.Connection, clusterConfig *config.ClusterConfig, 20 | node repository.Node, networkName, networkInterfaceMAC string, sshPublicKey string) error { 21 | 22 | storageVolume, err := connection.CreateStorageVolume(libvirt.CreateStorageVolumeParams{ 23 | Pool: node.StoragePool, 24 | Name: node.StorageVolume, 25 | CapacityGiB: node.VolumeCapacityGiB, 26 | BackingVolumeName: node.BackingStorageVolume, 27 | }) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | stageResult, err := clusterConfig.StageNode(node.Name, sshPublicKey) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | params := libvirt.DefineDomainParams{ 38 | Name: node.Domain, 39 | Network: networkName, 40 | NetworkInterfaceMAC: networkInterfaceMAC, 41 | StorageVolumePath: storageVolume.Target().Path(), 42 | MemoryMiB: node.MemoryMiB, 43 | CPUs: node.CPUs, 44 | FilesystemMounts: stageResult.FilesystemMounts, 45 | } 46 | 47 | return connection.DefineDomain(params) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/current.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func newCurrentCmd() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "current", 13 | Short: "Prints the current cluster", 14 | SilenceUsage: true, 15 | } 16 | 17 | cmd.RunE = currentCmdRunE 18 | return cmd 19 | } 20 | 21 | func currentCmdRunE(cmd *cobra.Command, args []string) error { 22 | if len(args) > 0 { 23 | return errors.New("invalid command arguments") 24 | } 25 | 26 | clusterRepository, err := newClusterRepository() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | current, err := clusterRepository.Current() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if current == nil { 37 | return nil 38 | } 39 | 40 | fmt.Println(current.Name) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /cmd/download/download.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/bzip2" 7 | "compress/gzip" 8 | "fmt" 9 | "io" 10 | "path" 11 | 12 | "github.com/bcrusu/kcm/libvirt" 13 | "github.com/bcrusu/kcm/repository" 14 | "github.com/bcrusu/kcm/util" 15 | ) 16 | 17 | func DownloadPrerequisites(connection *libvirt.Connection, cluster repository.Cluster, cacheDir string) error { 18 | if err := downloadKubernetes(cluster.KubernetesVersion, path.Join(cacheDir, "kubernetes")); err != nil { 19 | return err 20 | } 21 | 22 | if err := downloadCNI(cluster.CNIVersion, path.Join(cacheDir, "cni")); err != nil { 23 | return err 24 | } 25 | 26 | if err := downloadBackingStorageImage(connection, cluster); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func downloadBackingStorageImage(connection *libvirt.Connection, cluster repository.Cluster) error { 34 | volume, err := connection.GetStorageVolume(cluster.StoragePool, cluster.BackingStorageVolume) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | if volume != nil { 40 | // volume exists - will not download 41 | return nil 42 | } 43 | 44 | fmt.Printf("Downloading CoreOS image %s/%s...\n", cluster.CoreOSChannel, cluster.CoreOSVersion) 45 | 46 | bytes, err := downloadCoreOS(cluster.CoreOSChannel, cluster.CoreOSVersion) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | _, err = connection.CreateStorageVolume(libvirt.CreateStorageVolumeParams{ 52 | Pool: cluster.StoragePool, 53 | Name: cluster.BackingStorageVolume, 54 | CapacityGiB: 10, 55 | Content: bytes, 56 | }) 57 | 58 | return err 59 | } 60 | 61 | func downloadCoreOS(channel, version string) ([]byte, error) { 62 | url := fmt.Sprintf("https://%s.release.core-os.net/amd64-usr/%s/coreos_production_qemu_image.img.bz2", channel, version) 63 | 64 | downloadReader, err := util.DownloadHTTP(url) 65 | if err != nil { 66 | return nil, err 67 | } 68 | defer util.CloseNoError(downloadReader) 69 | 70 | decompressReader := bzip2.NewReader(downloadReader) 71 | buffer := &bytes.Buffer{} 72 | 73 | _, err = io.Copy(buffer, decompressReader) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return buffer.Bytes(), nil 79 | } 80 | 81 | func downloadTarGz(url string, outDir string) error { 82 | downloadReader, err := util.DownloadHTTP(url) 83 | if err != nil { 84 | return err 85 | } 86 | defer util.CloseNoError(downloadReader) 87 | 88 | gzipReader, err := gzip.NewReader(downloadReader) 89 | if err != nil { 90 | return err 91 | } 92 | defer util.CloseNoError(gzipReader) 93 | 94 | buffered := bufio.NewReader(gzipReader) 95 | return util.ExtractTar(buffered, outDir) 96 | } 97 | 98 | func downloadKubernetes(version string, cacheDir string) error { 99 | kubePath := path.Join(cacheDir, version) 100 | 101 | exists, err := util.DirectoryExists(kubePath) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if exists { 107 | // kubernetes version already on disk - no need to download 108 | return nil 109 | } 110 | 111 | fmt.Printf("Downloading Kubernetes %s...\n", version) 112 | 113 | url := fmt.Sprintf("https://dl.k8s.io/release/v%s/kubernetes-server-linux-amd64.tar.gz", version) 114 | return downloadTarGz(url, kubePath) 115 | } 116 | 117 | func downloadCNI(version string, cacheDir string) error { 118 | cniPath := path.Join(cacheDir, version) 119 | 120 | exists, err := util.DirectoryExists(cniPath) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if exists { 126 | // CNI already on disk - no need to download 127 | return nil 128 | } 129 | 130 | fmt.Printf("Downloading CNI %s...\n", version) 131 | 132 | url := fmt.Sprintf("https://dl.k8s.io/network-plugins/cni-amd64-%s.tar.gz", version) 133 | return downloadTarGz(url, cniPath) 134 | } 135 | -------------------------------------------------------------------------------- /cmd/kubectl.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | 8 | "github.com/bcrusu/kcm/util" 9 | "github.com/golang/glog" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func newKubectlCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "kubectl ARGS", 16 | Aliases: []string{"ctl", "kctl"}, 17 | Short: "Runs kubectl for the current cluster", 18 | SilenceUsage: true, 19 | DisableFlagParsing: true, 20 | } 21 | 22 | cmd.RunE = kubectlCmdRunE 23 | return cmd 24 | } 25 | 26 | func kubectlCmdRunE(cmd *cobra.Command, args []string) error { 27 | if len(args) == 1 && args[0] == "--help" { 28 | return cmd.Help() 29 | } 30 | 31 | for _, arg := range args { 32 | if strings.HasPrefix(arg, "--kubeconfig") { 33 | fmt.Println("Do not set 'kubeconfig' option. It will be set automatically by kcm") 34 | return nil 35 | } 36 | } 37 | 38 | clusterRepository, err := newClusterRepository() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | cluster, err := getWorkingCluster(clusterRepository, "") 44 | if err != nil { 45 | return err 46 | } 47 | 48 | kubectlPath := path.Join(kubernetesBinDir(cluster.KubernetesVersion), "kubectl") 49 | kubeconfigPath := path.Join(*dataDir, "config", cluster.Name, "kubeconfig", "kubectl") 50 | kubectlArgs := append(args, fmt.Sprintf(`--kubeconfig=%s`, kubeconfigPath)) 51 | 52 | if err := util.ExecCommandAndWait(kubectlPath, kubectlArgs...); err != nil { 53 | glog.Warningf("failed to execute kubectl. Error: %v", err) 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/cmd/list" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newListCmd() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "list", 12 | Aliases: []string{"ls"}, 13 | Short: "List clusters", 14 | SilenceUsage: true, 15 | } 16 | 17 | cmd.RunE = listCmdRunE 18 | return cmd 19 | } 20 | 21 | func listCmdRunE(cmd *cobra.Command, args []string) error { 22 | if len(args) > 0 { 23 | return errors.New("invalid command arguments") 24 | } 25 | 26 | clusterRepository, err := newClusterRepository() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | current, err := clusterRepository.Current() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | allClusters, err := clusterRepository.LoadAll() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | list.Print(allClusters, current) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/list/printer.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/bcrusu/kcm/repository" 7 | "github.com/bcrusu/kcm/util" 8 | ) 9 | 10 | func Print(clusters []*repository.Cluster, current *repository.Cluster) { 11 | Sort(clusters) 12 | 13 | writer := util.NewTabWriter(os.Stdout) 14 | 15 | writer.Print("CURRENT\tCLUSTER") 16 | writer.Nl() 17 | 18 | for _, cluster := range clusters { 19 | mark := "" 20 | if current != nil && cluster.Name == current.Name { 21 | mark = "*" 22 | } 23 | 24 | writer.Print(mark) 25 | writer.Tab() 26 | writer.Print(cluster.Name) 27 | 28 | writer.Nl() 29 | } 30 | 31 | writer.Flush() 32 | } 33 | -------------------------------------------------------------------------------- /cmd/list/sorter.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/bcrusu/kcm/repository" 8 | ) 9 | 10 | type sorter struct { 11 | Clusters []*repository.Cluster 12 | } 13 | 14 | func Sort(nodes []*repository.Cluster) { 15 | sorter := &sorter{nodes} 16 | sort.Sort(sorter) 17 | } 18 | 19 | func (s *sorter) Len() int { 20 | return len(s.Clusters) 21 | } 22 | 23 | func (s *sorter) Less(i, j int) bool { 24 | return strings.Compare(s.Clusters[i].Name, s.Clusters[j].Name) < 0 25 | } 26 | 27 | func (s *sorter) Swap(i, j int) { 28 | s.Clusters[i], s.Clusters[j] = s.Clusters[j], s.Clusters[i] 29 | } 30 | -------------------------------------------------------------------------------- /cmd/remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | func newRemoveCmd() *cobra.Command { 6 | cmd := &cobra.Command{ 7 | Use: "remove", 8 | Aliases: []string{"rm"}, 9 | Short: "Removes the specified object (deletes all data)", 10 | SilenceUsage: true, 11 | } 12 | 13 | cmd.AddCommand(newRemoveClusterCmd()) 14 | cmd.AddCommand(newRemoveNodeCmd()) 15 | 16 | return cmd 17 | } 18 | -------------------------------------------------------------------------------- /cmd/remove/cluster.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/config" 5 | "github.com/bcrusu/kcm/libvirt" 6 | "github.com/bcrusu/kcm/repository" 7 | ) 8 | 9 | func Cluster(connection *libvirt.Connection, clusterConfig *config.ClusterConfig, cluster repository.Cluster) error { 10 | for _, node := range cluster.Nodes { 11 | if err := Node(connection, clusterConfig, node); err != nil { 12 | return err 13 | } 14 | } 15 | 16 | if err := Network(connection, cluster.Network); err != nil { 17 | return err 18 | } 19 | 20 | return clusterConfig.UnstageCluster() 21 | } 22 | -------------------------------------------------------------------------------- /cmd/remove/network.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | func Network(connection *libvirt.Connection, network repository.Network) error { 9 | name := network.Name 10 | 11 | net, err := connection.GetNetwork(name) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | if net == nil { 17 | // network does not exist 18 | return nil 19 | } 20 | 21 | active, err := connection.NetworkIsActive(name) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if active { 27 | if err := connection.DestroyNetwork(name); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return connection.UndefineNetwork(name) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/remove/node.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/config" 5 | "github.com/bcrusu/kcm/libvirt" 6 | "github.com/bcrusu/kcm/repository" 7 | ) 8 | 9 | func Node(connection *libvirt.Connection, clusterConfig *config.ClusterConfig, node repository.Node) error { 10 | { 11 | domainName := node.Domain 12 | domain, err := connection.GetDomain(domainName) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if domain != nil { 18 | active, err := connection.DomainIsActive(domainName) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | if active { 24 | if err := connection.DestroyDomain(domainName); err != nil { 25 | return err 26 | } 27 | } 28 | 29 | if err := connection.UndefineDomain(domainName); err != nil { 30 | return err 31 | } 32 | } 33 | } 34 | 35 | { 36 | storageVolume, err := connection.GetStorageVolume(node.StoragePool, node.StorageVolume) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if storageVolume != nil { 42 | if err := connection.DeleteStorageVolume(node.StoragePool, node.StorageVolume); err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | 48 | if err := clusterConfig.UnstageNode(node.Name); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /cmd/remove_cluster.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/cmd/remove" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newRemoveClusterCmd() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "cluster CLUSTER_NAME", 12 | Aliases: []string{"c"}, 13 | Short: "Remove the specified clusters", 14 | SilenceUsage: true, 15 | } 16 | 17 | cmd.RunE = removeClusterCmdRunE 18 | return cmd 19 | } 20 | 21 | func removeClusterCmdRunE(cmd *cobra.Command, args []string) error { 22 | if len(args) != 1 { 23 | return errors.New("invalid command arguments") 24 | } 25 | 26 | clusterName := args[0] 27 | 28 | clusterRepository, err := newClusterRepository() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | cluster, err := clusterRepository.Load(clusterName) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if cluster == nil { 39 | return errors.Errorf("could not find cluster '%s'", clusterName) 40 | } 41 | 42 | connection, err := connectLibvirt() 43 | if err != nil { 44 | return err 45 | } 46 | defer connection.Close() 47 | 48 | clusterConfig, err := getClusterConfig(*cluster) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if err := remove.Cluster(connection, clusterConfig, *cluster); err != nil { 54 | return errors.Wrapf(err, "failed to remove cluster libvirt objects '%s'", clusterName) 55 | } 56 | 57 | if err := clusterRepository.Remove(cluster.Name); err != nil { 58 | return errors.Wrapf(err, "failed to remove cluster data '%s'", clusterName) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/remove_node.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/cmd/remove" 5 | "github.com/golang/glog" 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type removeNodeCmdState struct { 11 | ClusterName string 12 | } 13 | 14 | func newRemoveNodeCmd() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "node NODE_NAME", 17 | Aliases: []string{"n"}, 18 | Short: "Remove the specified cluster node", 19 | SilenceUsage: true, 20 | } 21 | 22 | state := &removeNodeCmdState{} 23 | cmd.PersistentFlags().StringVarP(&state.ClusterName, "cluster", "c", "", "Cluster that owns the node. If not specified, the current cluster will be used") 24 | 25 | cmd.RunE = state.runE 26 | return cmd 27 | } 28 | 29 | func (s *removeNodeCmdState) runE(cmd *cobra.Command, args []string) error { 30 | if len(args) != 1 { 31 | return errors.New("invalid command arguments") 32 | } 33 | 34 | nodeName := args[0] 35 | 36 | clusterRepository, err := newClusterRepository() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | cluster, err := getWorkingCluster(clusterRepository, s.ClusterName) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | toRemove, ok := cluster.Nodes[nodeName] 47 | if !ok { 48 | glog.Errorf("cluster '%s' does not contain node '%s'", cluster.Name, nodeName) 49 | return nil 50 | } 51 | 52 | connection, err := connectLibvirt() 53 | if err != nil { 54 | return err 55 | } 56 | defer connection.Close() 57 | 58 | clusterConfig, err := getClusterConfig(*cluster) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if err := remove.Node(connection, clusterConfig, toRemove); err != nil { 64 | return errors.Wrapf(err, "failed to remove node '%s' in cluster '%s'", nodeName, cluster.Name) 65 | } 66 | 67 | delete(cluster.Nodes, toRemove.Name) 68 | if err := clusterRepository.Save(*cluster); err != nil { 69 | return errors.Wrapf(err, "failed to persist state for cluster '%s' after removing node '%s'", cluster.Name, nodeName) 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/bcrusu/kcm/util" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const LibvirtDefaultURI = "qemu:///system" 11 | 12 | var ( 13 | dataDir = RootCmd.PersistentFlags().String("data-dir", getDefaultDataDir(), "kcm data directory") 14 | libvirtURI = RootCmd.PersistentFlags().String("libvirt-uri", LibvirtDefaultURI, "Libvirt URI") 15 | ) 16 | 17 | func init() { 18 | RootCmd.AddCommand(newCreateCmd()) 19 | RootCmd.AddCommand(newRemoveCmd()) 20 | RootCmd.AddCommand(newSwitchCmd()) 21 | RootCmd.AddCommand(newStartCmd()) 22 | RootCmd.AddCommand(newStopCmd()) 23 | RootCmd.AddCommand(newListCmd()) 24 | RootCmd.AddCommand(newCurrentCmd()) 25 | RootCmd.AddCommand(newAddNodeCmd()) 26 | RootCmd.AddCommand(newStatusCmd()) 27 | RootCmd.AddCommand(newKubectlCmd()) 28 | } 29 | 30 | var RootCmd = &cobra.Command{ 31 | Use: "kcm", 32 | SilenceUsage: true, 33 | } 34 | 35 | func getDefaultDataDir() string { 36 | home := util.GetUserHomeDir() 37 | return path.Join(home, ".kcm") 38 | } 39 | -------------------------------------------------------------------------------- /cmd/start.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/cmd/start" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newStartCmd() *cobra.Command { 10 | var cmd = &cobra.Command{ 11 | Use: "start [CLUSTER_NAME]", 12 | Short: "Starts the specified/current cluster", 13 | SilenceUsage: true, 14 | } 15 | 16 | cmd.RunE = startCmdRunE 17 | return cmd 18 | } 19 | 20 | func startCmdRunE(cmd *cobra.Command, args []string) error { 21 | if len(args) > 1 { 22 | return errors.New("invalid command arguments") 23 | } 24 | 25 | clusterRepository, err := newClusterRepository() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | clusterName := "" 31 | if len(args) == 1 { 32 | clusterName = args[0] 33 | } 34 | 35 | cluster, err := getWorkingCluster(clusterRepository, clusterName) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | connection, err := connectLibvirt() 41 | if err != nil { 42 | return err 43 | } 44 | defer connection.Close() 45 | 46 | if err := start.Cluster(connection, *cluster); err != nil { 47 | return errors.Wrapf(err, "failed to start cluster '%s'", cluster.Name) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/start/cluster.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | func Cluster(connection *libvirt.Connection, cluster repository.Cluster) error { 9 | if err := Network(connection, cluster.Network); err != nil { 10 | return err 11 | } 12 | 13 | // start masters first 14 | for _, node := range cluster.Nodes { 15 | if !node.IsMaster { 16 | continue 17 | } 18 | 19 | if err := Node(connection, node); err != nil { 20 | return err 21 | } 22 | } 23 | 24 | for _, node := range cluster.Nodes { 25 | if node.IsMaster { 26 | continue 27 | } 28 | 29 | if err := Node(connection, node); err != nil { 30 | return err 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /cmd/start/network.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func Network(connection *libvirt.Connection, network repository.Network) error { 10 | name := network.Name 11 | 12 | active, err := isNetworkActive(connection, name) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if active { 18 | // network is already running 19 | return nil 20 | } 21 | 22 | return connection.CreateNetwork(name) 23 | } 24 | 25 | func isNetworkActive(connection *libvirt.Connection, name string) (bool, error) { 26 | net, err := connection.GetNetwork(name) 27 | if err != nil { 28 | return false, err 29 | } 30 | 31 | if net == nil { 32 | return false, errors.Errorf("cannot find network '%s'", name) 33 | } 34 | 35 | active, err := connection.NetworkIsActive(name) 36 | if err != nil { 37 | return false, err 38 | } 39 | 40 | return active, nil 41 | } 42 | -------------------------------------------------------------------------------- /cmd/start/node.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | "github.com/golang/glog" 7 | ) 8 | 9 | func Node(connection *libvirt.Connection, node repository.Node) error { 10 | name := node.Domain 11 | 12 | domain, err := connection.GetDomain(name) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if domain == nil { 18 | glog.Warningf("cannot find domain '%s'", name) 19 | // ignore missing domains 20 | return nil 21 | } 22 | 23 | active, err := connection.DomainIsActive(name) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if active { 29 | // domain is already running 30 | return nil 31 | } 32 | 33 | return connection.CreateDomain(name) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/status.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bcrusu/kcm/cmd/status" 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func newStatusCmd() *cobra.Command { 12 | var cmd = &cobra.Command{ 13 | Use: "status [CLUSTER_NAME]", 14 | Aliases: []string{"stat"}, 15 | Short: "Prints the status for the specified/current cluster", 16 | SilenceUsage: true, 17 | } 18 | 19 | cmd.RunE = statusCmdRunE 20 | return cmd 21 | } 22 | 23 | func statusCmdRunE(cmd *cobra.Command, args []string) error { 24 | if len(args) > 1 { 25 | return errors.New("invalid command arguments") 26 | } 27 | 28 | clusterRepository, err := newClusterRepository() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | clusterName := "" 34 | if len(args) == 1 { 35 | clusterName = args[0] 36 | } 37 | 38 | cluster, err := getWorkingCluster(clusterRepository, clusterName) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | connection, err := connectLibvirt() 44 | if err != nil { 45 | return err 46 | } 47 | defer connection.Close() 48 | 49 | clusterStatus, err := status.Cluster(connection, *cluster) 50 | if err != nil { 51 | return errors.Wrapf(err, "failed to get status for cluster '%s'", cluster.Name) 52 | } 53 | 54 | status.PrintCluster(*clusterStatus) 55 | fmt.Println() 56 | 57 | status.PrintNetwork(clusterStatus.Network) 58 | fmt.Println() 59 | 60 | status.PrintNodes(clusterStatus.Nodes) 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/status/cluster.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | type ClusterStatus struct { 9 | Active bool 10 | Network NetworkStatus 11 | Nodes []NodeStatus 12 | Cluster repository.Cluster 13 | } 14 | 15 | func Cluster(connection *libvirt.Connection, cluster repository.Cluster) (*ClusterStatus, error) { 16 | netStatus, err := Network(connection, cluster.Network) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | var nodes []NodeStatus 22 | for _, node := range cluster.Nodes { 23 | nodeStatus, err := Node(connection, node) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | nodes = append(nodes, *nodeStatus) 29 | } 30 | 31 | return &ClusterStatus{ 32 | Active: netStatus.Active, 33 | Network: *netStatus, 34 | Nodes: nodes, 35 | Cluster: cluster, 36 | }, nil 37 | } 38 | 39 | func IsClusterActive(connection *libvirt.Connection, cluster repository.Cluster) (bool, error) { 40 | // simple check atm. - assume cluster is running if the network is active 41 | netStatus, err := Network(connection, cluster.Network) 42 | if err != nil { 43 | return false, err 44 | } 45 | 46 | return netStatus.Active, nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/status/network.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | type NetworkStatus struct { 9 | Active bool 10 | Missing bool 11 | Network repository.Network 12 | } 13 | 14 | func Network(connection *libvirt.Connection, network repository.Network) (*NetworkStatus, error) { 15 | net, err := connection.GetNetwork(network.Name) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | if net == nil { 21 | return &NetworkStatus{ 22 | Missing: true, 23 | Network: network, 24 | }, nil 25 | } 26 | 27 | active, err := connection.NetworkIsActive(network.Name) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &NetworkStatus{ 33 | Active: active, 34 | Network: network, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /cmd/status/node.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/bcrusu/kcm/libvirt" 7 | "github.com/bcrusu/kcm/repository" 8 | ) 9 | 10 | type NodeStatus struct { 11 | Active bool 12 | Missing bool 13 | Addresses []string 14 | DNSLookupErr bool 15 | Node repository.Node 16 | } 17 | 18 | func Node(connection *libvirt.Connection, node repository.Node) (*NodeStatus, error) { 19 | domain, err := connection.GetDomain(node.Domain) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if domain == nil { 25 | return &NodeStatus{ 26 | Missing: true, 27 | Node: node, 28 | }, nil 29 | } 30 | 31 | active, err := connection.DomainIsActive(node.Domain) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | var addresses []string 37 | var dnsLookupErr bool 38 | if active { 39 | addresses, err = connection.ListDomainInterfaceAddresses(node.Domain) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | _, err = net.LookupHost(node.DNSName) 45 | if err != nil { 46 | if _, ok := err.(*net.DNSError); ok { 47 | dnsLookupErr = true 48 | } else { 49 | return nil, err 50 | } 51 | } 52 | } 53 | 54 | return &NodeStatus{ 55 | Active: active, 56 | Missing: false, 57 | Addresses: addresses, 58 | DNSLookupErr: dnsLookupErr, 59 | Node: node, 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /cmd/status/printer.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/bcrusu/kcm/util" 9 | ) 10 | 11 | func PrintNodes(stats []NodeStatus) { 12 | Sort(stats) 13 | 14 | writer := util.NewTabWriter(os.Stdout) 15 | 16 | writer.Print("NODE\tSTATUS\tDNS NAME\tDNS LOOKUP\tIP") 17 | writer.Nl() 18 | 19 | for _, stat := range stats { 20 | writer.Print(stat.Node.Name) 21 | writer.Tab() 22 | 23 | if stat.Missing { 24 | writer.Print("Missing") 25 | } else if stat.Active { 26 | writer.Print("Active") 27 | } else { 28 | writer.Print("Inactive") 29 | } 30 | writer.Tab() 31 | 32 | writer.Print(stat.Node.DNSName) 33 | writer.Tab() 34 | 35 | if stat.DNSLookupErr { 36 | writer.Print("FAILED") 37 | } else { 38 | if stat.Active { 39 | writer.Print("OK") 40 | } else { 41 | writer.Print("-") 42 | } 43 | } 44 | writer.Tab() 45 | 46 | if len(stat.Addresses) > 0 { 47 | writer.Print(strings.Join(stat.Addresses, ", ")) 48 | } else { 49 | if stat.Active { 50 | writer.Print("waiting DHCP lease") 51 | } else { 52 | writer.Print("-") 53 | } 54 | } 55 | 56 | writer.Nl() 57 | } 58 | 59 | writer.Flush() 60 | } 61 | 62 | func PrintNetwork(stat NetworkStatus) { 63 | writer := util.NewTabWriter(os.Stdout) 64 | 65 | writer.Print("NETWORK\tSTATUS\tCIDR\tDNS SERVER") 66 | writer.Nl() 67 | 68 | writer.Print(stat.Network.Name) 69 | writer.Tab() 70 | 71 | if stat.Missing { 72 | writer.Print("Missing") 73 | } else if stat.Active { 74 | writer.Print("Active") 75 | } else { 76 | writer.Print("Inactive") 77 | } 78 | writer.Tab() 79 | 80 | writer.Print(stat.Network.IPv4CIDR) 81 | writer.Tab() 82 | 83 | { 84 | networkInfo, err := util.ParseNetworkCIDR(stat.Network.IPv4CIDR) 85 | if err != nil { 86 | panic("failed to parse network CIDR") 87 | } 88 | 89 | writer.Print(networkInfo.BridgeIP.String()) 90 | } 91 | writer.Nl() 92 | 93 | writer.Flush() 94 | } 95 | 96 | func PrintCluster(stat ClusterStatus) { 97 | writer := util.NewTabWriter(os.Stdout) 98 | 99 | writer.Print("CLUSTER\tSTATUS\tDNS DOMAIN\tKUBE VERSION\tCOREOS VERSION") 100 | writer.Nl() 101 | 102 | writer.Print(stat.Cluster.Name) 103 | writer.Tab() 104 | 105 | if stat.Active { 106 | writer.Print("Active") 107 | } else { 108 | writer.Print("Inactive") 109 | } 110 | writer.Tab() 111 | 112 | writer.Print(stat.Cluster.DNSDomain) 113 | writer.Tab() 114 | 115 | writer.Print(stat.Cluster.KubernetesVersion) 116 | writer.Tab() 117 | 118 | writer.Print(fmt.Sprintf("%s/%s", stat.Cluster.CoreOSChannel, stat.Cluster.CoreOSVersion)) 119 | writer.Nl() 120 | 121 | writer.Flush() 122 | } 123 | -------------------------------------------------------------------------------- /cmd/status/sorter.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | type nodeStatusSorter struct { 9 | Nodes []NodeStatus 10 | } 11 | 12 | func Sort(nodes []NodeStatus) { 13 | sorter := &nodeStatusSorter{nodes} 14 | sort.Sort(sorter) 15 | } 16 | 17 | func (s *nodeStatusSorter) Len() int { 18 | return len(s.Nodes) 19 | } 20 | 21 | func (s *nodeStatusSorter) Less(i, j int) bool { 22 | s1 := s.Nodes[i] 23 | s2 := s.Nodes[j] 24 | 25 | if s1.Node.IsMaster != s2.Node.IsMaster { 26 | // masters above minions (a hard m' fact of life) 27 | return s1.Node.IsMaster 28 | } 29 | 30 | return strings.Compare(s1.Node.Name, s2.Node.Name) < 0 31 | } 32 | 33 | func (s *nodeStatusSorter) Swap(i, j int) { 34 | s.Nodes[i], s.Nodes[j] = s.Nodes[j], s.Nodes[i] 35 | } 36 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/cmd/stop" 5 | "github.com/pkg/errors" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type stopCmdState struct { 10 | Force bool 11 | } 12 | 13 | func newStopCmd() *cobra.Command { 14 | var cmd = &cobra.Command{ 15 | Use: "stop [CLUSTER_NAME]", 16 | Short: "Stops the specified/current cluster", 17 | SilenceUsage: true, 18 | } 19 | 20 | state := &stopCmdState{} 21 | cmd.PersistentFlags().BoolVarP(&state.Force, "force", "f", false, "Does not use graceful shutdown (may produce inconsistent storage volume state)") 22 | 23 | cmd.RunE = state.runE 24 | return cmd 25 | } 26 | 27 | func (s stopCmdState) runE(cmd *cobra.Command, args []string) error { 28 | if len(args) > 1 { 29 | return errors.New("invalid command arguments") 30 | } 31 | 32 | clusterRepository, err := newClusterRepository() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | clusterName := "" 38 | if len(args) == 1 { 39 | clusterName = args[0] 40 | } 41 | 42 | cluster, err := getWorkingCluster(clusterRepository, clusterName) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | connection, err := connectLibvirt() 48 | if err != nil { 49 | return err 50 | } 51 | defer connection.Close() 52 | 53 | if err := stop.Cluster(connection, *cluster, s.Force); err != nil { 54 | return errors.Wrapf(err, "failed to stop cluster '%s'", cluster.Name) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/stop/cluster.go: -------------------------------------------------------------------------------- 1 | package stop 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | ) 7 | 8 | func Cluster(connection *libvirt.Connection, cluster repository.Cluster, force bool) error { 9 | for _, node := range cluster.Nodes { 10 | if err := Node(connection, node, force); err != nil { 11 | return err 12 | } 13 | } 14 | 15 | if err := Network(connection, cluster.Network); err != nil { 16 | return err 17 | } 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /cmd/stop/network.go: -------------------------------------------------------------------------------- 1 | package stop 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | "github.com/golang/glog" 7 | ) 8 | 9 | func Network(connection *libvirt.Connection, network repository.Network) error { 10 | name := network.Name 11 | 12 | net, err := connection.GetNetwork(name) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if net == nil { 18 | glog.Warningf("cannot find network '%s'", name) 19 | return nil 20 | } 21 | 22 | active, err := connection.NetworkIsActive(name) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if !active { 28 | return nil 29 | } 30 | 31 | return connection.DestroyNetwork(name) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/stop/node.go: -------------------------------------------------------------------------------- 1 | package stop 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | "github.com/golang/glog" 7 | ) 8 | 9 | func Node(connection *libvirt.Connection, node repository.Node, force bool) error { 10 | name := node.Domain 11 | 12 | domain, err := connection.GetDomain(name) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | if domain == nil { 18 | glog.Warningf("cannot find domain '%s'", name) 19 | // ignore missing domains 20 | return nil 21 | } 22 | 23 | active, err := connection.DomainIsActive(name) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if !active { 29 | return nil 30 | } 31 | 32 | if force { 33 | return connection.DestroyDomain(name) 34 | } 35 | 36 | return connection.ShutdownDomain(name) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/switch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type switchCmdState struct { 9 | Clear bool 10 | } 11 | 12 | func newSwitchCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "switch CLUSTER_NAME", 15 | Short: "Switches the current cluster", 16 | SilenceUsage: true, 17 | } 18 | 19 | state := &switchCmdState{} 20 | cmd.PersistentFlags().BoolVarP(&state.Clear, "clear", "c", false, "Clears the current cluster") 21 | 22 | cmd.RunE = state.runE 23 | return cmd 24 | } 25 | 26 | func (s *switchCmdState) runE(cmd *cobra.Command, args []string) error { 27 | clusterRepository, err := newClusterRepository() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if s.Clear { 33 | return clusterRepository.SetCurrent("") 34 | } 35 | 36 | if len(args) != 1 { 37 | return errors.New("invalid command arguments") 38 | } 39 | 40 | clusterName := args[0] 41 | 42 | { 43 | current, err := clusterRepository.Current() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if current != nil && current.Name == clusterName { 49 | // is already the current cluster 50 | return nil 51 | } 52 | } 53 | 54 | cluster, err := clusterRepository.Load(clusterName) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if cluster == nil { 60 | return errors.Errorf("could not find cluster '%s'", clusterName) 61 | } 62 | 63 | return clusterRepository.SetCurrent(cluster.Name) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | 8 | "github.com/bcrusu/kcm/config" 9 | "github.com/bcrusu/kcm/libvirt" 10 | "github.com/bcrusu/kcm/repository" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const MasterNodeNamePrefix = "master" 15 | const NodeNamePrefix = "node" 16 | 17 | func newClusterRepository() (repository.ClusterRepository, error) { 18 | repoPath := path.Join(*dataDir, "clusters") 19 | return repository.New(repoPath) 20 | } 21 | 22 | func cacheDir() string { 23 | return path.Join(*dataDir, "cache") 24 | } 25 | 26 | func connectLibvirt() (*libvirt.Connection, error) { 27 | return libvirt.NewConnection(*libvirtURI) 28 | } 29 | 30 | func libvirtNetworkName(clusterName string) string { 31 | return fmt.Sprintf("kcm.%s", clusterName) 32 | } 33 | 34 | func libvirtStorageVolumeName(domainName string) string { 35 | return fmt.Sprintf("%s.qcow2", domainName) 36 | } 37 | 38 | func libvirtDomainName(clusterName string, nodeName string) string { 39 | return fmt.Sprintf("kcm.%s.%s", clusterName, nodeName) 40 | } 41 | 42 | func coreOSStorageVolumeName(version string) string { 43 | return fmt.Sprintf("coreos_production_qemu_image_%s.qcow2", version) 44 | } 45 | 46 | func getWorkingCluster(clusterRepository repository.ClusterRepository, clusterName string) (*repository.Cluster, error) { 47 | var cluster *repository.Cluster 48 | var err error 49 | 50 | if clusterName != "" { 51 | cluster, err = clusterRepository.Load(clusterName) 52 | } else { 53 | cluster, err = clusterRepository.Current() 54 | } 55 | 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | if cluster == nil { 61 | if clusterName != "" { 62 | return nil, errors.Errorf("could not find cluster '%s'", clusterName) 63 | } 64 | 65 | return nil, errors.Errorf("current cluster is not set. Use the 'switch' command to set the current cluster") 66 | } 67 | 68 | return cluster, nil 69 | } 70 | 71 | func getClusterConfig(cluster repository.Cluster) (*config.ClusterConfig, error) { 72 | clusterDir := path.Join(*dataDir, "config", cluster.Name) 73 | 74 | kubernetesBinDir := kubernetesBinDir(cluster.KubernetesVersion) 75 | cniBinDir := path.Join(cacheDir(), "cni", cluster.CNIVersion, "bin") 76 | 77 | return config.New(clusterDir, cluster, kubernetesBinDir, cniBinDir) 78 | } 79 | 80 | func readSSHPublicKey(path string) (string, error) { 81 | bytes, err := ioutil.ReadFile(path) 82 | if err != nil { 83 | return "", errors.Wrapf(err, "cannot load SSH public key from file '%s'", path) 84 | } 85 | 86 | return string(bytes), nil 87 | } 88 | 89 | func nodeDNSName(nodeName string, clusterDomain string) string { 90 | return fmt.Sprintf("%s.%s", nodeName, clusterDomain) 91 | } 92 | 93 | func kubernetesBinDir(kubernetesVersion string) string { 94 | return path.Join(cacheDir(), "kubernetes", kubernetesVersion, "kubernetes", "server", "bin") 95 | } 96 | -------------------------------------------------------------------------------- /cmd/validate/libvirt.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirt" 5 | "github.com/bcrusu/kcm/repository" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func LibvirtClusterObjects(connection *libvirt.Connection, cluster repository.Cluster) error { 10 | storagePool, err := connection.GetStoragePool(cluster.StoragePool) 11 | if err != nil { 12 | return err 13 | } 14 | if storagePool == nil { 15 | return errors.Errorf("libvirt storage pool '%s' does not exist", cluster.StoragePool) 16 | } 17 | 18 | network, err := connection.GetNetwork(cluster.Network.Name) 19 | if err != nil { 20 | return err 21 | } 22 | if network != nil { 23 | return errors.Errorf("libvirt network '%s' already exists", cluster.Network.Name) 24 | } 25 | 26 | for _, node := range cluster.Nodes { 27 | if err := LibvirtNodeObjects(connection, node); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func LibvirtNodeObjects(connection *libvirt.Connection, node repository.Node) error { 36 | domain, err := connection.GetDomain(node.Domain) 37 | if err != nil { 38 | return err 39 | } 40 | if domain != nil { 41 | return errors.Errorf("libvirt domain '%s' already exists", node.Domain) 42 | } 43 | 44 | storageVolume, err := connection.GetStorageVolume(node.StoragePool, node.StorageVolume) 45 | if err != nil { 46 | return err 47 | } 48 | if storageVolume != nil { 49 | return errors.Errorf("libvirt storage volume '%s' already exists", node.StorageVolume) 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /config/addons/addons.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/bcrusu/kcm/util" 7 | ) 8 | 9 | type Params struct { 10 | ClusterDomain string 11 | DNSServiceIP string 12 | PodsNetworkCIDR string 13 | ProxyImageTag string 14 | FlannelImageTag string 15 | DNSImageTag string 16 | } 17 | 18 | func Write(outDir string, params Params) error { 19 | if err := util.CreateDirectoryPath(outDir); err != nil { 20 | return err 21 | } 22 | 23 | if err := util.WriteFile(path.Join(outDir, "kube-proxy.yaml"), 24 | util.GenerateTextTemplate(proxyTemplate, proxyTemplateParams{ 25 | ImageTag: params.ProxyImageTag, 26 | PodsNetworkCIDR: params.PodsNetworkCIDR, 27 | })); err != nil { 28 | return err 29 | } 30 | 31 | if err := util.WriteFile(path.Join(outDir, "flannel.yaml"), 32 | util.GenerateTextTemplate(flannelTemplate, flannelTemplateParams{ 33 | ImageTag: params.FlannelImageTag, 34 | PodsNetworkCIDR: params.PodsNetworkCIDR, 35 | })); err != nil { 36 | return err 37 | } 38 | 39 | if err := util.WriteFile(path.Join(outDir, "dns.yaml"), 40 | util.GenerateTextTemplate(dnsTemplate, dnsTemplateParams{ 41 | ServiceIP: params.DNSServiceIP, 42 | ClusterDomain: params.ClusterDomain, 43 | ImageTag: params.DNSImageTag, 44 | })); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /config/addons/dns.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | type dnsTemplateParams struct { 4 | ClusterDomain string 5 | ServiceIP string 6 | ImageTag string 7 | } 8 | 9 | const dnsTemplate = `kind: ServiceAccount 10 | apiVersion: v1 11 | metadata: 12 | name: kube-dns 13 | namespace: kube-system 14 | labels: 15 | k8s-app: kube-dns 16 | k8s-addon: kube-dns.addons.k8s.io 17 | --- 18 | kind: Service 19 | apiVersion: v1 20 | metadata: 21 | name: kube-dns 22 | namespace: kube-system 23 | labels: 24 | k8s-addon: kube-dns.addons.k8s.io 25 | k8s-app: kube-dns 26 | kubernetes.io/cluster-service: "true" 27 | kubernetes.io/name: "KubeDNS" 28 | spec: 29 | selector: 30 | k8s-app: kube-dns 31 | clusterIP: {{ .ServiceIP }} 32 | ports: 33 | - name: dns 34 | port: 53 35 | protocol: UDP 36 | - name: dns-tcp 37 | port: 53 38 | protocol: TCP 39 | --- 40 | kind: ConfigMap 41 | apiVersion: v1 42 | metadata: 43 | name: kube-dns 44 | namespace: kube-system 45 | labels: 46 | addonmanager.kubernetes.io/mode: EnsureExists 47 | --- 48 | kind: Deployment 49 | apiVersion: extensions/v1beta1 50 | metadata: 51 | name: kube-dns 52 | namespace: kube-system 53 | labels: 54 | k8s-addon: kube-dns.addons.k8s.io 55 | k8s-app: kube-dns 56 | kubernetes.io/cluster-service: "true" 57 | spec: 58 | replicas: 1 59 | strategy: 60 | rollingUpdate: 61 | maxSurge: 10% 62 | maxUnavailable: 0 63 | selector: 64 | matchLabels: 65 | k8s-app: kube-dns 66 | template: 67 | metadata: 68 | labels: 69 | k8s-app: kube-dns 70 | annotations: 71 | scheduler.alpha.kubernetes.io/critical-pod: '' 72 | spec: 73 | dnsPolicy: Default # Don't use cluster DNS. 74 | serviceAccountName: kube-dns 75 | volumes: 76 | - name: kube-dns-config 77 | configMap: 78 | name: kube-dns 79 | optional: true 80 | nodeSelector: 81 | beta.kubernetes.io/arch: amd64 82 | 83 | containers: 84 | - name: kubedns 85 | image: gcr.io/google_containers/k8s-dns-kube-dns-amd64:{{ .ImageTag }} 86 | resources: 87 | limits: 88 | memory: 170Mi 89 | requests: 90 | cpu: 100m 91 | memory: 70Mi 92 | livenessProbe: 93 | httpGet: 94 | path: /healthcheck/kubedns 95 | port: 10054 96 | scheme: HTTP 97 | initialDelaySeconds: 60 98 | timeoutSeconds: 5 99 | successThreshold: 1 100 | failureThreshold: 5 101 | readinessProbe: 102 | httpGet: 103 | path: /readiness 104 | port: 8081 105 | scheme: HTTP 106 | initialDelaySeconds: 3 107 | timeoutSeconds: 5 108 | args: 109 | - --domain={{ .ClusterDomain }}. 110 | - --dns-port=10053 111 | - --config-dir=/kube-dns-config 112 | - --v=2 113 | env: 114 | - name: PROMETHEUS_PORT 115 | value: "10055" 116 | ports: 117 | - containerPort: 10053 118 | name: dns-local 119 | protocol: UDP 120 | - containerPort: 10053 121 | name: dns-tcp-local 122 | protocol: TCP 123 | - containerPort: 10055 124 | name: metrics 125 | protocol: TCP 126 | volumeMounts: 127 | - name: kube-dns-config 128 | mountPath: /kube-dns-config 129 | 130 | - name: dnsmasq 131 | image: gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:{{ .ImageTag }} 132 | livenessProbe: 133 | httpGet: 134 | path: /healthcheck/dnsmasq 135 | port: 10054 136 | scheme: HTTP 137 | initialDelaySeconds: 60 138 | timeoutSeconds: 5 139 | successThreshold: 1 140 | failureThreshold: 5 141 | args: 142 | - -v=2 143 | - -logtostderr 144 | - -configDir=/etc/k8s/dns/dnsmasq-nanny 145 | - -restartDnsmasq=true 146 | - -- 147 | - -k 148 | - --cache-size=1000 149 | - --log-facility=- 150 | - --server=/{{ .ClusterDomain }}/127.0.0.1#10053 151 | - --server=/in-addr.arpa/127.0.0.1#10053 152 | - --server=/in6.arpa/127.0.0.1#10053 153 | ports: 154 | - containerPort: 53 155 | name: dns 156 | protocol: UDP 157 | - containerPort: 53 158 | name: dns-tcp 159 | protocol: TCP 160 | resources: 161 | requests: 162 | cpu: 150m 163 | memory: 20Mi 164 | volumeMounts: 165 | - name: kube-dns-config 166 | mountPath: /etc/k8s/dns/dnsmasq-nanny 167 | 168 | - name: sidecar 169 | image: gcr.io/google_containers/k8s-dns-sidecar-amd64:{{ .ImageTag }} 170 | livenessProbe: 171 | httpGet: 172 | path: /metrics 173 | port: 10054 174 | scheme: HTTP 175 | initialDelaySeconds: 60 176 | timeoutSeconds: 5 177 | successThreshold: 1 178 | failureThreshold: 5 179 | args: 180 | - --v=2 181 | - --logtostderr 182 | - --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.{{ .ClusterDomain }},5,A 183 | - --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.{{ .ClusterDomain }},5,A 184 | ports: 185 | - containerPort: 10054 186 | name: metrics 187 | protocol: TCP 188 | resources: 189 | requests: 190 | memory: 20Mi 191 | cpu: 10m 192 | ` 193 | -------------------------------------------------------------------------------- /config/addons/flannel.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | type flannelTemplateParams struct { 4 | ImageTag string 5 | PodsNetworkCIDR string 6 | } 7 | 8 | const flannelTemplate = `kind: ServiceAccount 9 | apiVersion: v1 10 | metadata: 11 | name: flannel 12 | namespace: kube-system 13 | labels: 14 | role.kubernetes.io/networking: "1" 15 | --- 16 | kind: ConfigMap 17 | apiVersion: v1 18 | metadata: 19 | name: kube-flannel-cfg 20 | namespace: kube-system 21 | labels: 22 | k8s-app: flannel 23 | role.kubernetes.io/networking: "1" 24 | data: 25 | cni-conf.json: | 26 | { 27 | "name": "cbr0", 28 | "type": "flannel", 29 | "delegate": { 30 | "bridge": "cbr0", 31 | "forceAddress": true, 32 | "isDefaultGateway": true, 33 | "hairpinMode": true 34 | } 35 | } 36 | net-conf.json: | 37 | { 38 | "Network": "{{ .PodsNetworkCIDR }}", 39 | "Backend": { 40 | "Type": "udp" 41 | } 42 | } 43 | --- 44 | kind: DaemonSet 45 | apiVersion: extensions/v1beta1 46 | metadata: 47 | name: kube-flannel-ds 48 | namespace: kube-system 49 | labels: 50 | k8s-app: flannel 51 | role.kubernetes.io/networking: "1" 52 | spec: 53 | template: 54 | metadata: 55 | labels: 56 | tier: node 57 | app: flannel 58 | role.kubernetes.io/networking: "1" 59 | spec: 60 | hostNetwork: true 61 | nodeSelector: 62 | beta.kubernetes.io/arch: amd64 63 | serviceAccountName: flannel 64 | tolerations: 65 | - effect: NoSchedule 66 | key: node-role.kubernetes.io/master 67 | containers: 68 | - name: kube-flannel 69 | image: quay.io/coreos/flannel:{{ .ImageTag }} 70 | command: [ "/opt/bin/flanneld", "--ip-masq", "--kube-subnet-mgr" ] 71 | securityContext: 72 | privileged: true 73 | env: 74 | - name: POD_NAME 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: metadata.name 78 | - name: POD_NAMESPACE 79 | valueFrom: 80 | fieldRef: 81 | fieldPath: metadata.namespace 82 | resources: 83 | limits: 84 | cpu: 100m 85 | memory: 100Mi 86 | requests: 87 | cpu: 100m 88 | memory: 100Mi 89 | volumeMounts: 90 | - name: run 91 | mountPath: /run 92 | - name: flannel-cfg 93 | mountPath: /etc/kube-flannel/ 94 | - name: install-cni 95 | image: quay.io/coreos/flannel:{{ .ImageTag }} 96 | command: [ "/bin/sh", "-c", "set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done" ] 97 | resources: 98 | limits: 99 | cpu: 10m 100 | memory: 25Mi 101 | requests: 102 | cpu: 10m 103 | memory: 25Mi 104 | volumeMounts: 105 | - name: cni 106 | mountPath: /etc/cni/net.d 107 | - name: flannel-cfg 108 | mountPath: /etc/kube-flannel/ 109 | volumes: 110 | - name: run 111 | hostPath: 112 | path: /run 113 | - name: cni 114 | hostPath: 115 | path: /etc/cni/net.d 116 | - name: flannel-cfg 117 | configMap: 118 | name: kube-flannel-cfg 119 | ` 120 | -------------------------------------------------------------------------------- /config/addons/proxy.go: -------------------------------------------------------------------------------- 1 | package addons 2 | 3 | type proxyTemplateParams struct { 4 | ImageTag string 5 | PodsNetworkCIDR string 6 | } 7 | 8 | const proxyTemplate = `kind: ServiceAccount 9 | apiVersion: v1 10 | metadata: 11 | name: kube-proxy 12 | namespace: kube-system 13 | --- 14 | kind: DaemonSet 15 | apiVersion: extensions/v1beta1 16 | metadata: 17 | name: kube-proxy 18 | namespace: kube-system 19 | spec: 20 | template: 21 | metadata: 22 | labels: 23 | k8s-app: kube-proxy 24 | spec: 25 | hostNetwork: true 26 | nodeSelector: 27 | beta.kubernetes.io/arch: amd64 28 | tolerations: 29 | - effect: NoSchedule 30 | key: node-role.kubernetes.io/master 31 | containers: 32 | - name: kube-proxy 33 | image: gcr.io/google_containers/kube-proxy:{{ .ImageTag }} 34 | securityContext: 35 | privileged: true 36 | command: 37 | - kube-proxy 38 | - "--bind-address=127.0.0.1" 39 | - "--kubeconfig=/opt/kubernetes/kubeconfig/kube-proxy" 40 | - "--cluster-cidr={{ .PodsNetworkCIDR }}" 41 | volumeMounts: 42 | - name: opt-kubernetes 43 | mountPath: "/opt/kubernetes" 44 | readOnly: true 45 | livenessProbe: 46 | httpGet: 47 | scheme: HTTP 48 | host: 127.0.0.1 49 | port: 10249 50 | path: "/healthz" 51 | initialDelaySeconds: 15 52 | timeoutSeconds: 15 53 | serviceAccountName: kube-proxy 54 | volumes: 55 | - name: opt-kubernetes 56 | hostPath: 57 | path: /opt/kubernetes 58 | ` 59 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/bcrusu/kcm/config/coreos" 7 | "github.com/bcrusu/kcm/config/scripts" 8 | "github.com/bcrusu/kcm/libvirt" 9 | "github.com/bcrusu/kcm/repository" 10 | "github.com/bcrusu/kcm/util" 11 | "github.com/golang/glog" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type ClusterConfig struct { 16 | clusterDir string 17 | cluster repository.Cluster 18 | kubernetesBinDir string 19 | cniBinDir string 20 | 21 | // private/k8s network 22 | podsNetworkCIDR string 23 | servicesNetworkCIDR string 24 | apiServerServiceIP string 25 | dnsServiceIP string 26 | 27 | // public/libvirt network 28 | Network util.NetworkInfo 29 | } 30 | 31 | type StageNodeResult struct { 32 | FilesystemMounts []libvirt.FilesystemMount 33 | } 34 | 35 | func New(clusterDir string, cluster repository.Cluster, kubernetesBinDir, cniBinDir string) (*ClusterConfig, error) { 36 | err := util.CreateDirectoryPath(clusterDir) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | network, err := util.ParseNetworkCIDR(cluster.Network.IPv4CIDR) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &ClusterConfig{ 47 | clusterDir: clusterDir, 48 | cluster: cluster, 49 | kubernetesBinDir: kubernetesBinDir, 50 | cniBinDir: cniBinDir, 51 | podsNetworkCIDR: "10.2.0.0/17", 52 | servicesNetworkCIDR: "10.2.128.0/17", 53 | apiServerServiceIP: "10.2.128.1", // k8s convention, the API server service gets the 1st IP 54 | dnsServiceIP: "10.2.128.10", // could be any IP in the servicesNetworkCIDR range 55 | Network: *network, 56 | }, nil 57 | } 58 | 59 | func (c ClusterConfig) StageNode(name string, sshPublicKey string) (*StageNodeResult, error) { 60 | node, ok := c.cluster.Nodes[name] 61 | if !ok { 62 | return nil, errors.Errorf("cluster '%s' does not contain node '%s'", c.cluster.Name, name) 63 | } 64 | 65 | nodeDir := c.nodeConfigDir(node.Name) 66 | if err := prepareDirectory(nodeDir); err != nil { 67 | return nil, err 68 | } 69 | 70 | if err := c.stageKubernetesForNode(path.Join(nodeDir, "kubernetes"), node); err != nil { 71 | return nil, err 72 | } 73 | 74 | if err := c.stageCoreOS(path.Join(nodeDir, "coreos"), node, sshPublicKey); err != nil { 75 | return nil, err 76 | } 77 | 78 | return &StageNodeResult{ 79 | FilesystemMounts: c.getFilesystemMounts(nodeDir), 80 | }, nil 81 | } 82 | 83 | func (c ClusterConfig) UnstageNode(name string) error { 84 | node, ok := c.cluster.Nodes[name] 85 | if !ok { 86 | return errors.Errorf("cluster '%s' does not contain node '%s'", c.cluster.Name, name) 87 | } 88 | 89 | nodeDir := c.nodeConfigDir(node.Name) 90 | exists, err := util.DirectoryExists(nodeDir) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if !exists { 96 | return nil 97 | } 98 | 99 | if err := util.RemoveDirectory(nodeDir); err != nil { 100 | return errors.Wrapf(err, "failed to remove node config directory '%s'", nodeDir) 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (c ClusterConfig) StageCluster() error { 107 | if err := prepareDirectory(c.clusterDir); err != nil { 108 | return err 109 | } 110 | 111 | if err := c.stageKubernetesForCluster(c.clusterDir); err != nil { 112 | return err 113 | } 114 | 115 | if err := scripts.Write(path.Join(c.clusterDir, "scripts")); err != nil { 116 | return err 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (c ClusterConfig) UnstageCluster() error { 123 | exists, err := util.DirectoryExists(c.clusterDir) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | if !exists { 129 | return nil 130 | } 131 | 132 | if err := util.RemoveDirectory(c.clusterDir); err != nil { 133 | return errors.Wrapf(err, "failed to remove cluster config directory '%s'", c.clusterDir) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (c ClusterConfig) getFilesystemMounts(nodeDir string) []libvirt.FilesystemMount { 140 | return []libvirt.FilesystemMount{ 141 | libvirt.FilesystemMount{ 142 | HostPath: path.Join(nodeDir, "coreos"), 143 | GuestPath: "config-2", 144 | }, 145 | libvirt.FilesystemMount{ 146 | HostPath: path.Join(nodeDir, "kubernetes"), 147 | GuestPath: "k8sConfig", 148 | }, 149 | libvirt.FilesystemMount{ 150 | HostPath: c.kubernetesBinDir, 151 | GuestPath: "k8sBin", 152 | }, 153 | libvirt.FilesystemMount{ 154 | HostPath: path.Join(c.clusterDir, "manifests"), 155 | GuestPath: "k8sConfigManifests", 156 | }, 157 | libvirt.FilesystemMount{ 158 | HostPath: path.Join(c.clusterDir, "kubeconfig"), 159 | GuestPath: "k8sConfigKubeconfig", 160 | }, 161 | libvirt.FilesystemMount{ 162 | HostPath: path.Join(c.clusterDir, "addons"), 163 | GuestPath: "k8sConfigAddons", 164 | }, 165 | libvirt.FilesystemMount{ 166 | HostPath: c.cniBinDir, 167 | GuestPath: "cniBin", 168 | }, 169 | libvirt.FilesystemMount{ 170 | HostPath: path.Join(c.clusterDir, "scripts"), 171 | GuestPath: "scripts", 172 | }, 173 | } 174 | } 175 | 176 | func (c ClusterConfig) stageCoreOS(outDir string, node repository.Node, sshPublicKey string) error { 177 | params := coreos.CloudConfigParams{ 178 | Hostname: node.Name, 179 | DNSName: node.DNSName, 180 | IsMaster: node.IsMaster, 181 | SSHPublicKey: sshPublicKey, 182 | Network: c.Network, 183 | ClusterDomain: c.cluster.DNSDomain, 184 | DNSServiceIP: c.dnsServiceIP, 185 | } 186 | 187 | if err := coreos.WriteCoreOSConfig(outDir, params); err != nil { 188 | return err 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (c ClusterConfig) nodeConfigDir(nodeName string) string { 195 | return path.Join(c.clusterDir, "nodes", nodeName) 196 | } 197 | 198 | func prepareDirectory(dir string) error { 199 | exists, err := util.DirectoryExists(dir) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | if exists { 205 | glog.Warningf("config directory '%s' exists - will be deleted", dir) 206 | 207 | if err := util.RemoveDirectory(dir); err != nil { 208 | return errors.Wrapf(err, "failed to remove config directory '%s'", dir) 209 | } 210 | } 211 | 212 | if err := util.CreateDirectoryPath(dir); err != nil { 213 | return errors.Wrapf(err, "failed to create config directory '%s'", dir) 214 | } 215 | 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /config/coreos/cloudConfig.go: -------------------------------------------------------------------------------- 1 | package coreos 2 | 3 | import "github.com/bcrusu/kcm/util" 4 | 5 | type CloudConfigParams struct { 6 | Hostname string 7 | DNSName string 8 | IsMaster bool 9 | SSHPublicKey string 10 | Network util.NetworkInfo 11 | ClusterDomain string 12 | DNSServiceIP string 13 | } 14 | 15 | const cloudConfigTemplate = `#cloud-config 16 | 17 | hostname: {{ .Hostname }} 18 | 19 | ssh_authorized_keys: 20 | - {{ .SSHPublicKey }} 21 | 22 | write_files: 23 | - path: /etc/systemd/journald.conf 24 | permissions: 0644 25 | content: | 26 | [Journal] 27 | SystemMaxUse=50M 28 | RuntimeMaxUse=50M 29 | 30 | coreos: 31 | {{ if .IsMaster }} 32 | etcd2: 33 | advertise-client-urls: http://0.0.0.0:2379 34 | listen-client-urls: http://0.0.0.0:2379 35 | listen-peer-urls: http://0.0.0.0:2380 36 | initial-cluster-state: new 37 | initial-cluster: {{ .Hostname }}=http://0.0.0.0:2380 38 | initial-advertise-peer-urls: http://0.0.0.0:2380 39 | {{ end }} 40 | 41 | units: 42 | {{ if .IsMaster }} 43 | - name: etcd2.service 44 | command: start 45 | drop-ins: 46 | - name: 10-override-name.conf 47 | content: | 48 | [Service] 49 | Environment=ETCD_NAME=%H 50 | {{ end }} 51 | 52 | - name: cbr0.netdev 53 | command: start 54 | content: | 55 | [NetDev] 56 | Kind=bridge 57 | Name=cbr0 58 | - name: dhcp.network 59 | command: start 60 | content: | 61 | [Match] 62 | Name=eth0 63 | [Network] 64 | DHCP=yes 65 | SendHostname=true 66 | 67 | - name: docker.service 68 | command: start 69 | drop-ins: 70 | - name: 50-opts.conf 71 | content: | 72 | [Service] 73 | Environment='DOCKER_OPTS=--bridge=cbr0 --iptables=false --ip-masq=false' 74 | Environment='DOCKER_NOFILE=1000000' 75 | - name: docker-tcp.socket 76 | command: start 77 | enable: yes 78 | content: | 79 | [Unit] 80 | Description=Docker Socket for the API 81 | [Socket] 82 | ListenStream=2375 83 | BindIPv6Only=both 84 | Service=docker.service 85 | [Install] 86 | WantedBy=sockets.target 87 | 88 | - name: load-kernel-modules.service 89 | command: start 90 | content: | 91 | [Unit] 92 | Description=Load kernel modules needed by k8s 93 | 94 | [Service] 95 | Type=simple 96 | KillMode=process 97 | ExecStart=/usr/sbin/modprobe br_netfilter 98 | 99 | [Install] 100 | WantedBy=multi-user.target 101 | 102 | - name: opt-kubernetes.mount 103 | command: start 104 | content: | 105 | [Unit] 106 | ConditionVirtualization=|vm 107 | [Mount] 108 | What=k8sConfig 109 | Where=/opt/kubernetes 110 | Options=ro,trans=virtio,version=9p2000.L 111 | Type=9p 112 | - name: opt-kubernetes-bin.mount 113 | command: start 114 | content: | 115 | [Unit] 116 | ConditionVirtualization=|vm 117 | After=opt-kubernetes.mount 118 | Requires=opt-kubernetes.mount 119 | [Mount] 120 | What=k8sBin 121 | Where=/opt/kubernetes/bin 122 | Options=ro,trans=virtio,version=9p2000.L 123 | Type=9p 124 | - name: opt-kubernetes-manifests.mount 125 | command: start 126 | content: | 127 | [Unit] 128 | ConditionVirtualization=|vm 129 | After=opt-kubernetes.mount 130 | Requires=opt-kubernetes.mount 131 | [Mount] 132 | What=k8sConfigManifests 133 | Where=/opt/kubernetes/manifests 134 | Options=ro,trans=virtio,version=9p2000.L 135 | Type=9p 136 | - name: opt-kubernetes-kubeconfig.mount 137 | command: start 138 | content: | 139 | [Unit] 140 | ConditionVirtualization=|vm 141 | After=opt-kubernetes.mount 142 | Requires=opt-kubernetes.mount 143 | [Mount] 144 | What=k8sConfigKubeconfig 145 | Where=/opt/kubernetes/kubeconfig 146 | Options=ro,trans=virtio,version=9p2000.L 147 | Type=9p 148 | - name: opt-kubernetes-addons.mount 149 | command: start 150 | content: | 151 | [Unit] 152 | ConditionVirtualization=|vm 153 | After=opt-kubernetes.mount 154 | Requires=opt-kubernetes.mount 155 | [Mount] 156 | What=k8sConfigAddons 157 | Where=/opt/kubernetes/addons 158 | Options=ro,trans=virtio,version=9p2000.L 159 | Type=9p 160 | - name: opt-cni-bin.mount 161 | command: start 162 | content: | 163 | [Unit] 164 | ConditionVirtualization=|vm 165 | [Mount] 166 | What=cniBin 167 | Where=/opt/cni/bin 168 | Options=ro,trans=virtio,version=9p2000.L 169 | Type=9p 170 | - name: opt-scripts.mount 171 | command: start 172 | content: | 173 | [Unit] 174 | ConditionVirtualization=|vm 175 | [Mount] 176 | What=scripts 177 | Where=/opt/scripts 178 | Options=ro,trans=virtio,version=9p2000.L 179 | Type=9p 180 | - name: allVmMounts.target 181 | command: start 182 | content: | 183 | [Unit] 184 | After=opt-kubernetes.mount opt-kubernetes-bin.mount opt-kubernetes-manifests.mount opt-kubernetes-kubeconfig.mount opt-kubernetes-addons.mount opt-cni-bin.mount opt-scripts.mount 185 | Requires=opt-kubernetes.mount opt-kubernetes-bin.mount opt-kubernetes-manifests.mount opt-kubernetes-kubeconfig.mount opt-kubernetes-addons.mount opt-cni-bin.mount opt-scripts.mount 186 | 187 | - name: kubelet.service 188 | command: start 189 | content: | 190 | [Unit] 191 | After=allVmMounts.target docker.service load-k8s-images.service 192 | ConditionFileIsExecutable=/opt/kubernetes/bin/kubelet 193 | Description=Kubernetes Kubelet Server 194 | Documentation=https://github.com/kubernetes/kubernetes 195 | Requires=allVmMounts.target docker.service load-k8s-images.service 196 | 197 | [Service] 198 | Restart=always 199 | RestartSec=2 200 | StartLimitInterval=0 201 | KillMode=process 202 | Environment="PATH=/opt/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 203 | ExecStart=/opt/kubernetes/bin/kubelet \ 204 | --address=0.0.0.0 \ 205 | --hostname-override={{ .DNSName }} \ 206 | --cluster-dns={{ .DNSServiceIP }} \ 207 | --cluster-domain={{ .ClusterDomain }} \ 208 | --kubeconfig=/opt/kubernetes/kubeconfig/kubelet \ 209 | --require-kubeconfig=true \ 210 | --anonymous-auth=false \ 211 | --register-node=true \ 212 | --node-labels='kubernetes.io/role={{ Role }},node-role.kubernetes.io/{{ Role }}=' \ 213 | --network-plugin=cni \ 214 | --cni-bin-dir=/opt/cni/bin \ 215 | --cni-conf-dir=/etc/cni/net.d \ 216 | --allow-privileged=true \ 217 | --pod-manifest-path=/opt/kubernetes/manifests \ 218 | --tls-cert-file=/opt/kubernetes/certs/tls-server.pem \ 219 | --tls-private-key-file=/opt/kubernetes/certs/tls-server-key.pem \ 220 | --client-ca-file=/opt/kubernetes/certs/ca.pem {{ if .IsMaster }}\ 221 | --register-with-taints='node-role.kubernetes.io/master=:NoSchedule'{{ end }} 222 | 223 | [Install] 224 | WantedBy=multi-user.target 225 | 226 | - name: load-k8s-images.service 227 | command: start 228 | content: | 229 | [Unit] 230 | Description=Load Kubernetes images to Docker 231 | After=opt-kubernetes-bin.mount docker.service 232 | Requires=opt-kubernetes-bin.mount docker.service 233 | 234 | [Service] 235 | WorkingDirectory=/opt/kubernetes/bin 236 | Type=simple 237 | KillMode=process 238 | {{ if .IsMaster }} 239 | ExecStart=/bin/bash -c 'docker load -i kube-apiserver.tar && docker load -i kube-controller-manager.tar && docker load -i kube-scheduler.tar && docker load -i kube-proxy.tar' 240 | {{ else }} 241 | ExecStart=/usr/bin/docker load -i kube-proxy.tar 242 | {{ end }} 243 | 244 | {{ if .IsMaster }} 245 | - name: apply-k8s-addons.service 246 | command: start 247 | content: | 248 | [Unit] 249 | Description=Apply Kubernetes addons 250 | After=kubelet.service 251 | Requires=kubelet.service 252 | 253 | [Service] 254 | Type=simple 255 | KillMode=process 256 | ExecStart=/opt/scripts/load_addons 257 | 258 | [Install] 259 | WantedBy=multi-user.target 260 | {{ end }} 261 | 262 | - name: install-socat.service 263 | command: start 264 | content: | 265 | [Unit] 266 | Description=Install socat binary 267 | After=opt-scripts.mount 268 | Requires=opt-scripts.mount 269 | 270 | [Service] 271 | Type=simple 272 | KillMode=process 273 | ExecStart=/opt/scripts/install_socat 274 | 275 | [Install] 276 | WantedBy=multi-user.target 277 | ` 278 | -------------------------------------------------------------------------------- /config/coreos/coreos.go: -------------------------------------------------------------------------------- 1 | package coreos 2 | 3 | import ( 4 | "bytes" 5 | "path" 6 | "text/template" 7 | 8 | "github.com/bcrusu/kcm/util" 9 | ) 10 | 11 | func WriteCoreOSConfig(outDir string, params CloudConfigParams) error { 12 | userDataDir := path.Join(outDir, "openstack", "latest") 13 | if err := util.CreateDirectoryPath(userDataDir); err != nil { 14 | return err 15 | } 16 | 17 | data := generateCoreOSConfig(params) 18 | 19 | userDataFilename := path.Join(userDataDir, "user_data") 20 | if err := util.WriteFile(userDataFilename, data); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func generateCoreOSConfig(params CloudConfigParams) []byte { 28 | t := template.New("coreos") 29 | 30 | t.Funcs(template.FuncMap{ 31 | "Role": func() string { 32 | if params.IsMaster { 33 | return "master" 34 | } 35 | 36 | return "node" 37 | }, 38 | }) 39 | 40 | if _, err := t.Parse(cloudConfigTemplate); err != nil { 41 | panic(err) 42 | } 43 | 44 | buffer := &bytes.Buffer{} 45 | if err := t.ExecuteTemplate(buffer, "coreos", params); err != nil { 46 | panic(err) 47 | } 48 | 49 | return buffer.Bytes() 50 | } 51 | -------------------------------------------------------------------------------- /config/kubeconfig/kubeconfig.go: -------------------------------------------------------------------------------- 1 | package kubeconfig 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "path" 7 | 8 | "github.com/bcrusu/kcm/repository" 9 | "github.com/bcrusu/kcm/util" 10 | "github.com/ghodss/yaml" 11 | ) 12 | 13 | func WriteKubeconfigFiles(outDir string, cluster repository.Cluster) error { 14 | if err := util.CreateDirectoryPath(outDir); err != nil { 15 | return err 16 | } 17 | 18 | caCert, err := util.ParseCertificate(cluster.CACertificate) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | caKey, err := util.ParsePrivateKey(cluster.CAPrivateKey) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if err := generateKubeconfigFile(path.Join(outDir, "kubelet"), "kubelet", cluster, caCert, caKey); err != nil { 29 | return err 30 | } 31 | 32 | if err := generateKubeconfigFile(path.Join(outDir, "kube-scheduler"), KubeScheduler, cluster, caCert, caKey); err != nil { 33 | return err 34 | } 35 | 36 | if err := generateKubeconfigFile(path.Join(outDir, "kube-controller-manager"), KubeControllerManager, cluster, caCert, caKey); err != nil { 37 | return err 38 | } 39 | 40 | if err := generateKubeconfigFile(path.Join(outDir, "kube-proxy"), KubeProxy, cluster, caCert, caKey); err != nil { 41 | return err 42 | } 43 | 44 | if err := generateKubeconfigFile(path.Join(outDir, "kubectl"), "kubectl", cluster, caCert, caKey); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func generateKubeconfigFile(filename string, user string, cluster repository.Cluster, caCert *x509.Certificate, caKey *rsa.PrivateKey) error { 52 | clientCert, clientKey, err := util.CreateClientCertificate(user, caCert, caKey) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | bytes, err := CreateKubeconfig(user, cluster.Name, cluster.ServerURL, cluster.CACertificate, clientCert, clientKey) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return util.WriteFile(filename, bytes) 63 | } 64 | 65 | func CreateKubeconfig(user, clusterName, serverURL string, caCert, clientCert, clientKey []byte) ([]byte, error) { 66 | config := &KubectlConfig{ 67 | ApiVersion: "v1", 68 | Kind: "Config", 69 | Users: []*KubectlUserWithName{ 70 | { 71 | Name: user, 72 | User: KubectlUser{ 73 | ClientCertificateData: clientCert, 74 | ClientKeyData: clientKey, 75 | }, 76 | }, 77 | }, 78 | Clusters: []*KubectlClusterWithName{ 79 | { 80 | Name: clusterName, 81 | Cluster: KubectlCluster{ 82 | CertificateAuthorityData: caCert, 83 | Server: serverURL, 84 | }, 85 | }, 86 | }, 87 | Contexts: []*KubectlContextWithName{ 88 | { 89 | Name: "ctx", 90 | Context: KubectlContext{ 91 | Cluster: clusterName, 92 | User: user, 93 | }, 94 | }, 95 | }, 96 | CurrentContext: "ctx", 97 | } 98 | 99 | return yaml.Marshal(config) 100 | } 101 | -------------------------------------------------------------------------------- /config/kubeconfig/types.go: -------------------------------------------------------------------------------- 1 | package kubeconfig 2 | 3 | // borrowed from github.com/kubernetes/kops 4 | type KubectlConfig struct { 5 | Kind string `json:"kind"` 6 | ApiVersion string `json:"apiVersion"` 7 | CurrentContext string `json:"current-context"` 8 | Clusters []*KubectlClusterWithName `json:"clusters"` 9 | Contexts []*KubectlContextWithName `json:"contexts"` 10 | Users []*KubectlUserWithName `json:"users"` 11 | } 12 | 13 | type KubectlClusterWithName struct { 14 | Name string `json:"name"` 15 | Cluster KubectlCluster `json:"cluster"` 16 | } 17 | 18 | type KubectlCluster struct { 19 | Server string `json:"server,omitempty"` 20 | CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` 21 | } 22 | 23 | type KubectlContextWithName struct { 24 | Name string `json:"name"` 25 | Context KubectlContext `json:"context"` 26 | } 27 | 28 | type KubectlContext struct { 29 | Cluster string `json:"cluster"` 30 | User string `json:"user"` 31 | } 32 | 33 | type KubectlUserWithName struct { 34 | Name string `json:"name"` 35 | User KubectlUser `json:"user"` 36 | } 37 | 38 | type KubectlUser struct { 39 | ClientCertificateData []byte `json:"client-certificate-data,omitempty"` 40 | ClientKeyData []byte `json:"client-key-data,omitempty"` 41 | Password string `json:"password,omitempty"` 42 | Username string `json:"username,omitempty"` 43 | Token string `json:"token,omitempty"` 44 | } 45 | -------------------------------------------------------------------------------- /config/kubeconfig/users.go: -------------------------------------------------------------------------------- 1 | package kubeconfig 2 | 3 | // taken from: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/authentication/user/user.go 4 | // well-known user and group names 5 | const ( 6 | SystemPrivilegedGroup = "system:masters" 7 | NodesGroup = "system:nodes" 8 | AllUnauthenticated = "system:unauthenticated" 9 | AllAuthenticated = "system:authenticated" 10 | 11 | Anonymous = "system:anonymous" 12 | APIServerUser = "system:apiserver" 13 | 14 | // core kubernetes process identities 15 | KubeProxy = "system:kube-proxy" 16 | KubeControllerManager = "system:kube-controller-manager" 17 | KubeScheduler = "system:kube-scheduler" 18 | ) 19 | -------------------------------------------------------------------------------- /config/kubernetes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | 7 | "github.com/bcrusu/kcm/config/addons" 8 | "github.com/bcrusu/kcm/config/kubeconfig" 9 | "github.com/bcrusu/kcm/config/manifests" 10 | "github.com/bcrusu/kcm/repository" 11 | "github.com/bcrusu/kcm/util" 12 | ) 13 | 14 | type dockerImageTags struct { 15 | APIServer string 16 | ControllerManager string 17 | Scheduler string 18 | Proxy string 19 | Flannel string 20 | DNS string 21 | } 22 | 23 | func (c ClusterConfig) stageKubernetesForNode(outDir string, node repository.Node) error { 24 | if err := util.CreateDirectoryPath(outDir); err != nil { 25 | return err 26 | } 27 | 28 | { 29 | // create mount points 30 | mountPoints := []string{"bin", "manifests", "addons", "kubeconfig"} 31 | for _, mountPoint := range mountPoints { 32 | if err := util.CreateDirectoryPath(path.Join(outDir, mountPoint)); err != nil { 33 | return err 34 | } 35 | } 36 | } 37 | 38 | if err := c.writeCertificates(path.Join(outDir, "certs"), node); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (c ClusterConfig) stageKubernetesForCluster(outDir string) error { 46 | imageTags, err := c.getDockerImageTags() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if err := manifests.Write(path.Join(outDir, "manifests"), manifests.Params{ 52 | ClusterName: c.cluster.Name, 53 | PodsNetworkCIDR: c.podsNetworkCIDR, 54 | ServicesNetworkCIDR: c.servicesNetworkCIDR, 55 | APIServerImageTag: imageTags.APIServer, 56 | ControllerManagerImageTag: imageTags.ControllerManager, 57 | SchedulerImageTag: imageTags.Scheduler, 58 | }); err != nil { 59 | return err 60 | } 61 | 62 | if err := addons.Write(path.Join(outDir, "addons"), addons.Params{ 63 | ClusterDomain: c.cluster.DNSDomain, 64 | DNSServiceIP: c.dnsServiceIP, 65 | PodsNetworkCIDR: c.podsNetworkCIDR, 66 | ProxyImageTag: imageTags.Proxy, 67 | FlannelImageTag: imageTags.Flannel, 68 | DNSImageTag: imageTags.DNS, 69 | }); err != nil { 70 | return err 71 | } 72 | 73 | if err := kubeconfig.WriteKubeconfigFiles(path.Join(outDir, "kubeconfig"), c.cluster); err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (c ClusterConfig) getDockerImageTags() (*dockerImageTags, error) { 81 | result := &dockerImageTags{ 82 | Flannel: "v0.7.1", 83 | DNS: "1.14.2", 84 | } 85 | 86 | var err error 87 | readTag := func(fileName string) (string, error) { 88 | bytes, err := ioutil.ReadFile(path.Join(c.kubernetesBinDir, fileName)) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | return string(bytes), nil 94 | } 95 | 96 | if result.APIServer, err = readTag("kube-apiserver.docker_tag"); err != nil { 97 | return nil, err 98 | } 99 | 100 | if result.ControllerManager, err = readTag("kube-controller-manager.docker_tag"); err != nil { 101 | return nil, err 102 | } 103 | 104 | if result.Proxy, err = readTag("kube-proxy.docker_tag"); err != nil { 105 | return nil, err 106 | } 107 | 108 | if result.Scheduler, err = readTag("kube-scheduler.docker_tag"); err != nil { 109 | return nil, err 110 | } 111 | 112 | return result, nil 113 | } 114 | 115 | func (c ClusterConfig) writeCertificates(outDir string, node repository.Node) error { 116 | if err := util.CreateDirectoryPath(outDir); err != nil { 117 | return err 118 | } 119 | 120 | if err := util.WriteFile(path.Join(outDir, "ca.pem"), c.cluster.CACertificate); err != nil { 121 | return err 122 | } 123 | 124 | if err := util.WriteFile(path.Join(outDir, "ca-key.pem"), c.cluster.CAPrivateKey); err != nil { 125 | return err 126 | } 127 | 128 | caCert, err := util.ParseCertificate(c.cluster.CACertificate) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | caKey, err := util.ParsePrivateKey(c.cluster.CAPrivateKey) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | if node.IsMaster { 139 | clientCert, clientKey, err := util.CreateClientCertificate(node.DNSName, caCert, caKey) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | if err := util.WriteFile(path.Join(outDir, "tls-client.pem"), clientCert); err != nil { 145 | return err 146 | } 147 | 148 | if err := util.WriteFile(path.Join(outDir, "tls-client-key.pem"), clientKey); err != nil { 149 | return err 150 | } 151 | } 152 | 153 | { 154 | hosts := []string{ 155 | node.DNSName, 156 | } 157 | 158 | if node.IsMaster { 159 | hosts = append(hosts, []string{ 160 | c.apiServerServiceIP, 161 | "kubernetes", 162 | "kubernetes.default", 163 | "kubernetes.default.svc", 164 | "kubernetes.default.svc." + c.cluster.DNSDomain, 165 | }...) 166 | } 167 | 168 | serverCert, serverKey, err := util.CreateServerCertificate(node.DNSName, caCert, caKey, hosts...) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | if err := util.WriteFile(path.Join(outDir, "tls-server.pem"), serverCert); err != nil { 174 | return err 175 | } 176 | 177 | if err := util.WriteFile(path.Join(outDir, "tls-server-key.pem"), serverKey); err != nil { 178 | return err 179 | } 180 | } 181 | 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /config/manifests/apiServer.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | type apiServerTemplateParams struct { 4 | ImageTag string 5 | ServicesNetworkCIDR string 6 | } 7 | 8 | const apiServerTemplate = `kind: Pod 9 | apiVersion: v1 10 | metadata: 11 | name: kube-apiserver 12 | namespace: kube-system 13 | spec: 14 | hostNetwork: true 15 | affinity: 16 | nodeAffinity: 17 | requiredDuringSchedulingIgnoredDuringExecution: 18 | nodeSelectorTerms: 19 | - matchExpressions: 20 | - key: node-role.kubernetes.io/master 21 | operator: Exists 22 | containers: 23 | - name: kube-apiserver 24 | image: gcr.io/google_containers/kube-apiserver:{{ .ImageTag }} 25 | command: 26 | - kube-apiserver 27 | - "--apiserver-count=1" 28 | - "--allow-privileged=true" 29 | - "--etcd-servers=http://127.0.0.1:2379" 30 | - "--bind-address=0.0.0.0" 31 | - "--secure-port=6443" 32 | - "--anonymous-auth=false" 33 | - "--tls-cert-file=/opt/kubernetes/certs/tls-server.pem" 34 | - "--tls-private-key-file=/opt/kubernetes/certs/tls-server-key.pem" 35 | - "--tls-ca-file=/opt/kubernetes/certs/ca.pem" 36 | - "--client-ca-file=/opt/kubernetes/certs/ca.pem" 37 | - "--kubelet-certificate-authority=/opt/kubernetes/certs/ca.pem" 38 | - "--kubelet-client-certificate=/opt/kubernetes/certs/tls-client.pem" 39 | - "--kubelet-client-key=/opt/kubernetes/certs/tls-client-key.pem" 40 | - "--service-cluster-ip-range={{ .ServicesNetworkCIDR }}" 41 | - "--storage-backend=etcd2" 42 | - "--storage-media-type=application/json" 43 | - "--service-account-key-file=/opt/kubernetes/certs/tls-server-key.pem" 44 | - "--admission-control=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,ResourceQuota" 45 | ports: 46 | - name: https 47 | hostPort: 443 48 | containerPort: 443 49 | - name: local 50 | hostPort: 8080 51 | containerPort: 8080 52 | volumeMounts: 53 | - name: srvkube 54 | mountPath: "/srv/kubernetes" 55 | readOnly: true 56 | - name: etcssl 57 | mountPath: "/etc/ssl" 58 | readOnly: true 59 | - name: opt-kubernetes 60 | mountPath: "/opt/kubernetes" 61 | readOnly: true 62 | livenessProbe: 63 | httpGet: 64 | scheme: HTTP 65 | host: 127.0.0.1 66 | port: 8080 67 | path: "/healthz" 68 | initialDelaySeconds: 15 69 | timeoutSeconds: 15 70 | volumes: 71 | - name: srvkube 72 | hostPath: 73 | path: "/srv/kubernetes" 74 | - name: etcssl 75 | hostPath: 76 | path: "/etc/ssl" 77 | - name: opt-kubernetes 78 | hostPath: 79 | path: /opt/kubernetes 80 | ` 81 | -------------------------------------------------------------------------------- /config/manifests/controllerManager.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | type controllerManagerTemplateParams struct { 4 | ImageTag string 5 | ClusterName string 6 | PodsNetworkCIDR string 7 | } 8 | 9 | const controllerManagerTemplate = `kind: Pod 10 | apiVersion: v1 11 | metadata: 12 | name: kube-controller-manager 13 | namespace: kube-system 14 | spec: 15 | hostNetwork: true 16 | affinity: 17 | nodeAffinity: 18 | requiredDuringSchedulingIgnoredDuringExecution: 19 | nodeSelectorTerms: 20 | - matchExpressions: 21 | - key: node-role.kubernetes.io/master 22 | operator: Exists 23 | containers: 24 | - name: kube-controller-manager 25 | image: gcr.io/google_containers/kube-controller-manager:{{ .ImageTag }} 26 | command: 27 | - kube-controller-manager 28 | - "--address=127.0.0.1" 29 | - "--kubeconfig=/opt/kubernetes/kubeconfig/kube-controller-manager" 30 | - "--cluster-name={{ .ClusterName }}" 31 | - "--root-ca-file=/opt/kubernetes/certs/ca.pem" 32 | - "--cluster-signing-cert-file=/opt/kubernetes/certs/ca.pem" 33 | - "--cluster-signing-key-file=/opt/kubernetes/certs/ca-key.pem" 34 | - "--use-service-account-credentials=true" 35 | - "--allocate-node-cidrs=true" 36 | - "--cluster-cidr={{ .PodsNetworkCIDR }}" 37 | - "--leader-elect=true" 38 | - "--controllers=*,serviceaccount-token,bootstrapsigner,tokencleaner" 39 | - "--service-account-private-key-file=/opt/kubernetes/certs/tls-server-key.pem" 40 | volumeMounts: 41 | - name: srvkube 42 | mountPath: "/srv/kubernetes" 43 | readOnly: true 44 | - name: etcssl 45 | mountPath: "/etc/ssl" 46 | readOnly: true 47 | - name: opt-kubernetes 48 | mountPath: "/opt/kubernetes" 49 | readOnly: true 50 | livenessProbe: 51 | httpGet: 52 | scheme: HTTP 53 | host: 127.0.0.1 54 | port: 10252 55 | path: "/healthz" 56 | initialDelaySeconds: 15 57 | timeoutSeconds: 15 58 | volumes: 59 | - name: srvkube 60 | hostPath: 61 | path: "/srv/kubernetes" 62 | - name: etcssl 63 | hostPath: 64 | path: "/etc/ssl" 65 | - name: opt-kubernetes 66 | hostPath: 67 | path: /opt/kubernetes 68 | ` 69 | -------------------------------------------------------------------------------- /config/manifests/metadata.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/bcrusu/kcm/util" 7 | ) 8 | 9 | type Params struct { 10 | ClusterName string 11 | PodsNetworkCIDR string 12 | ServicesNetworkCIDR string 13 | 14 | APIServerImageTag string 15 | ControllerManagerImageTag string 16 | SchedulerImageTag string 17 | } 18 | 19 | func Write(outDir string, params Params) error { 20 | if err := util.CreateDirectoryPath(outDir); err != nil { 21 | return err 22 | } 23 | 24 | if err := util.WriteFile(path.Join(outDir, "kube-apiserver.yaml"), 25 | util.GenerateTextTemplate(apiServerTemplate, apiServerTemplateParams{ 26 | ImageTag: params.APIServerImageTag, 27 | ServicesNetworkCIDR: params.ServicesNetworkCIDR, 28 | })); err != nil { 29 | return err 30 | } 31 | 32 | if err := util.WriteFile(path.Join(outDir, "kube-controller-manager.yaml"), 33 | util.GenerateTextTemplate(controllerManagerTemplate, controllerManagerTemplateParams{ 34 | ImageTag: params.ControllerManagerImageTag, 35 | ClusterName: params.ClusterName, 36 | PodsNetworkCIDR: params.PodsNetworkCIDR, 37 | })); err != nil { 38 | return err 39 | } 40 | 41 | if err := util.WriteFile(path.Join(outDir, "kube-scheduler.yaml"), 42 | util.GenerateTextTemplate(schedulerTemplate, schedulerTemplateParams{ 43 | ImageTag: params.SchedulerImageTag, 44 | })); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /config/manifests/scheduler.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | type schedulerTemplateParams struct { 4 | ImageTag string 5 | } 6 | 7 | const schedulerTemplate = `kind: Pod 8 | apiVersion: v1 9 | metadata: 10 | name: kube-scheduler 11 | namespace: kube-system 12 | spec: 13 | hostNetwork: true 14 | affinity: 15 | nodeAffinity: 16 | requiredDuringSchedulingIgnoredDuringExecution: 17 | nodeSelectorTerms: 18 | - matchExpressions: 19 | - key: node-role.kubernetes.io/master 20 | operator: Exists 21 | containers: 22 | - name: kube-scheduler 23 | image: gcr.io/google_containers/kube-scheduler:{{ .ImageTag }} 24 | command: 25 | - kube-scheduler 26 | - "--address=127.0.0.1" 27 | - "--kubeconfig=/opt/kubernetes/kubeconfig/kube-scheduler" 28 | - "--leader-elect=true" 29 | volumeMounts: 30 | - name: opt-kubernetes 31 | mountPath: "/opt/kubernetes" 32 | readOnly: true 33 | livenessProbe: 34 | httpGet: 35 | scheme: HTTP 36 | host: 127.0.0.1 37 | port: 10251 38 | path: "/healthz" 39 | initialDelaySeconds: 15 40 | timeoutSeconds: 15 41 | volumes: 42 | - name: opt-kubernetes 43 | hostPath: 44 | path: /opt/kubernetes 45 | ` 46 | -------------------------------------------------------------------------------- /config/scripts/installSocat.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | // https://gist.github.com/cdemers/be415cb46327e56c5c47f9689a07a456 4 | const installSocat = `#! /bin/bash 5 | 6 | if [ -e /opt/bin/socat.d/bin/socat ] 7 | then 8 | echo socat binary is already installed. Skipping install... 9 | exit 0 10 | fi 11 | 12 | # Make socat directories 13 | mkdir -p /opt/bin/socat.d/bin /opt/bin/socat.d/lib 14 | 15 | # Create socat wrapper 16 | cat << EOF > /opt/bin/socat 17 | #! /bin/bash 18 | PATH=/usr/bin:/bin:/usr/sbin:/sbin:/opt/bin 19 | LD_LIBRARY_PATH=/opt/bin/socat.d/lib:$LD_LIBRARY_PATH exec /opt/bin/socat.d/bin/socat "\$@" 20 | EOF 21 | 22 | chmod +x /opt/bin/socat 23 | 24 | # Get socat and libraries from the CoreOS toolbox 25 | cat < 4 | 512 5 | 2 6 | 7 | hvm 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | destroy 17 | restart 18 | restart 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | ` 42 | -------------------------------------------------------------------------------- /libvirt/network.go: -------------------------------------------------------------------------------- 1 | package libvirt 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirtxml" 5 | "github.com/bcrusu/kcm/util" 6 | "github.com/golang/glog" 7 | "github.com/libvirt/libvirt-go" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type DefineNetworkParams struct { 12 | Name string 13 | IPv4CIDR string 14 | Domain string 15 | Metadata map[string]string // map[NAME]VALUE 16 | } 17 | 18 | func lookupNetwork(connect *libvirt.Connect, lookup string) (*libvirt.Network, error) { 19 | if len(lookup) == uuidStringLength { 20 | net, err := connect.LookupNetworkByUUIDString(lookup) 21 | if err != nil { 22 | if lverr, ok := err.(libvirt.Error); ok && lverr.Code != libvirt.ERR_NO_NETWORK { 23 | glog.Infof("libvirt: network lookup by ID '%s' failed. Error: %v", lookup, lverr) 24 | } 25 | } 26 | 27 | if net != nil { 28 | return net, nil 29 | } 30 | } 31 | 32 | net, err := connect.LookupNetworkByName(lookup) 33 | if err != nil { 34 | if lverr, ok := err.(libvirt.Error); ok && lverr.Code == libvirt.ERR_NO_NETWORK { 35 | return nil, nil 36 | } 37 | 38 | return nil, errors.Wrapf(err, "libvirt: network lookup failed '%s'", lookup) 39 | } 40 | 41 | return net, nil 42 | } 43 | 44 | func lookupNetworkStrict(connect *libvirt.Connect, lookup string) (*libvirt.Network, error) { 45 | net, err := lookupNetwork(connect, lookup) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | if net == nil { 51 | return nil, errors.Errorf("libvirt: could not find network '%s'", lookup) 52 | } 53 | 54 | return net, nil 55 | } 56 | 57 | func getNetworkXML(network *libvirt.Network) (*libvirtxml.Network, error) { 58 | xml, err := network.GetXMLDesc(libvirt.NetworkXMLFlags(0)) 59 | if err != nil { 60 | return nil, errors.Wrapf(err, "libvirt: failed to fetch network XML description") 61 | } 62 | 63 | return libvirtxml.NewNetworkForXML(xml) 64 | } 65 | 66 | func defineNATNetwork(connect *libvirt.Connect, params DefineNetworkParams) error { 67 | networkXML := libvirtxml.NewNetwork() 68 | networkXML.SetName(params.Name) 69 | networkXML.Forward().SetMode("nat") 70 | networkXML.Forward().SetNATPortRange(1024, 65535) 71 | 72 | networkXML.Bridge().SetSTP(true) 73 | 74 | networkXML.Domain().SetName(params.Domain) 75 | networkXML.Domain().SetLocalOnly(true) 76 | 77 | if params.IPv4CIDR != "" { 78 | addIP(networkXML, params.IPv4CIDR) 79 | } 80 | 81 | if len(networkXML.IPs()) == 0 { 82 | return errors.New("libvirt: failed to define network - missing CIDR") 83 | } 84 | 85 | setMetadataValues(networkXML.Metadata(), params.Metadata) 86 | 87 | xmlString, err := networkXML.MarshalToXML() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | network, err := connect.NetworkDefineXML(xmlString) 93 | if err != nil { 94 | return errors.Wrapf(err, "libvirt: failed to define network '%s'", params.Name) 95 | } 96 | defer network.Free() 97 | 98 | return err 99 | } 100 | 101 | func addIP(network libvirtxml.Network, cidr string) error { 102 | networkInfo, err := util.ParseNetworkCIDR(cidr) 103 | if err != nil { 104 | return errors.Wrapf(err, "libvirt: failed to define network - invalid CIDR '%s'", cidr) 105 | } 106 | 107 | if networkInfo.Family != "ipv4" { 108 | return errors.Errorf("libvirt: IPv6 network not supported '%s'", cidr) 109 | } 110 | 111 | prefix, bits := networkInfo.Net.Mask.Size() 112 | if bits-prefix < 3 { 113 | return errors.Wrapf(err, "libvirt: failed to define network - network is too small '%s'", cidr) 114 | } 115 | 116 | ipXML := network.NewIP() 117 | ipXML.SetFamily(networkInfo.Family) 118 | ipXML.SetAddress(networkInfo.BridgeIP.String()) 119 | ipXML.SetPrefix(prefix) 120 | 121 | ipXML.SetDHCPRange(networkInfo.DHCPRangeStart.String(), networkInfo.DHCPRangeEnd.String()) 122 | 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /libvirt/pool.go: -------------------------------------------------------------------------------- 1 | package libvirt 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirtxml" 5 | "github.com/golang/glog" 6 | "github.com/libvirt/libvirt-go" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func lookupStoragePool(connect *libvirt.Connect, lookup string) (*libvirt.StoragePool, error) { 11 | if len(lookup) == uuidStringLength { 12 | pool, err := connect.LookupStoragePoolByUUIDString(lookup) 13 | if err != nil { 14 | if lverr, ok := err.(libvirt.Error); ok && lverr.Code != libvirt.ERR_NO_STORAGE_POOL { 15 | glog.Infof("libvirt: storage pool lookup by ID '%s' failed. Error: %v", lookup, lverr) 16 | } 17 | } 18 | 19 | if pool != nil { 20 | return pool, nil 21 | } 22 | } 23 | 24 | pool, err := connect.LookupStoragePoolByName(lookup) 25 | if err != nil { 26 | if lverr, ok := err.(libvirt.Error); ok && lverr.Code == libvirt.ERR_NO_STORAGE_POOL { 27 | return nil, nil 28 | } 29 | 30 | return nil, errors.Wrapf(err, "libvirt: storage pool lookup failed '%s'", lookup) 31 | } 32 | 33 | return pool, nil 34 | } 35 | 36 | func lookupStoragePoolStrict(connect *libvirt.Connect, lookup string) (*libvirt.StoragePool, error) { 37 | pool, err := lookupStoragePool(connect, lookup) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if pool == nil { 43 | return nil, errors.Errorf("libvirt: could not find storage pool '%s'", lookup) 44 | } 45 | 46 | return pool, nil 47 | } 48 | 49 | func getStoragePoolXML(pool *libvirt.StoragePool) (*libvirtxml.StoragePool, error) { 50 | xml, err := pool.GetXMLDesc(libvirt.StorageXMLFlags(0)) 51 | if err != nil { 52 | return nil, errors.Wrapf(err, "libvirt: failed to fetch storage pool XML description") 53 | } 54 | 55 | return libvirtxml.NewStoragePoolForXML(xml) 56 | } 57 | -------------------------------------------------------------------------------- /libvirt/stream.go: -------------------------------------------------------------------------------- 1 | package libvirt 2 | 3 | import ( 4 | "github.com/libvirt/libvirt-go" 5 | ) 6 | 7 | const streamSendMaxSize = 16000000 // must be <= VIR_NET_MESSAGE_PAYLOAD_MAX 8 | 9 | func streamSendAll(stream *libvirt.Stream, bytes []byte) error { 10 | for { 11 | chunkSize := streamSendMaxSize 12 | if chunkSize > len(bytes) { 13 | chunkSize = len(bytes) 14 | } 15 | 16 | if chunkSize == 0 { 17 | break 18 | } 19 | 20 | chunk := bytes[:chunkSize] 21 | bytes = bytes[chunkSize:] 22 | 23 | _, err := stream.Send(chunk) 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /libvirt/util.go: -------------------------------------------------------------------------------- 1 | package libvirt 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bcrusu/kcm/libvirtxml" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const uuidStringLength = 36 15 | 16 | var random *rand.Rand 17 | 18 | func init() { 19 | source := rand.NewSource(time.Now().UnixNano()) 20 | random = rand.New(source) 21 | } 22 | 23 | func randomMACAddress(uri string) (string, error) { 24 | url, err := url.Parse(uri) 25 | if err != nil { 26 | return "", errors.Wrapf(err, "libvirt: failed to parse libvirt connection uri") 27 | } 28 | 29 | var mac []byte 30 | 31 | if isQemuURL(url) { 32 | mac = []byte{0x52, 0x54, 0x00} 33 | } else if isXenURL(url) { 34 | mac = []byte{0x00, 0x16, 0x3E} 35 | } 36 | 37 | for len(mac) < 6 { 38 | b := random.Uint32() 39 | mac = append(mac, byte(b)) 40 | } 41 | 42 | result := fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) 43 | return strings.ToUpper(result), nil 44 | } 45 | 46 | func isQemuURL(url *url.URL) bool { 47 | return strings.HasPrefix(url.Scheme, "qemu") 48 | } 49 | 50 | func isXenURL(url *url.URL) bool { 51 | return strings.HasPrefix(url.Scheme, "xen") || 52 | strings.HasPrefix(url.Scheme, "libxl") 53 | } 54 | 55 | func setMetadataValues(metadata libvirtxml.Metadata, kv map[string]string) { 56 | for name, value := range kv { 57 | nodeName := libvirtxml.NewName(MetadataXMLNamespace, name) 58 | node := metadata.NewNode(nodeName) 59 | node.CharData = value 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libvirt/volume.go: -------------------------------------------------------------------------------- 1 | package libvirt 2 | 3 | import ( 4 | "github.com/bcrusu/kcm/libvirtxml" 5 | "github.com/libvirt/libvirt-go" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type CreateStorageVolumeParams struct { 10 | Pool string 11 | Name string 12 | CapacityGiB uint 13 | 14 | // only one should be set 15 | BackingVolumeName string 16 | Content []byte 17 | } 18 | 19 | func lookupStorageVolume(pool *libvirt.StoragePool, lookup string) (*libvirt.StorageVol, error) { 20 | volume, err := pool.LookupStorageVolByName(lookup) 21 | if err != nil { 22 | if lverr, ok := err.(libvirt.Error); ok && lverr.Code == libvirt.ERR_NO_STORAGE_VOL { 23 | return nil, nil 24 | } 25 | return nil, errors.Wrapf(err, "libvirt: storage volume lookup failed '%s'", lookup) 26 | } 27 | 28 | return volume, nil 29 | } 30 | 31 | func getStorageVolumeXML(volume *libvirt.StorageVol) (*libvirtxml.StorageVolume, error) { 32 | xml, err := volume.GetXMLDesc(0) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "libvirt: failed to fetch storage volume XML description") 35 | } 36 | 37 | return libvirtxml.NewStorageVolumeForXML(xml) 38 | } 39 | 40 | func createStorageVolumeFromBackingVolume(pool *libvirt.StoragePool, params CreateStorageVolumeParams) (*libvirtxml.StorageVolume, error) { 41 | backingVolume, err := lookupStorageVolume(pool, params.BackingVolumeName) 42 | { 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if backingVolume == nil { 48 | return nil, errors.Errorf("libvirt: could not find storage volume '%s'", params.BackingVolumeName) 49 | } 50 | defer backingVolume.Free() 51 | } 52 | 53 | volumeXML, err := getStorageVolumeXML(backingVolume) 54 | { 55 | // create the new volume XML definition starting from the backing volume definition 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | volumeType := volumeXML.Type() 61 | if volumeType != "file" { 62 | errors.Errorf("libvirt: cannot clone storage volume '%s' - unsupported volume type '%s'", params.BackingVolumeName, volumeType) 63 | } 64 | 65 | volumeXML.SetName(params.Name) 66 | volumeXML.SetKey("") 67 | 68 | volumeXML.Capacity().SetUnit("GiB") 69 | volumeXML.Capacity().SetValue(uint64(params.CapacityGiB)) 70 | 71 | targetXML := volumeXML.Target() 72 | targetXML.RemoveTimestamps() 73 | 74 | sourcePath := targetXML.Path() 75 | targetXML.SetPath("") // will be filled-in by libvirt 76 | 77 | { 78 | // set backing store as the souorce target 79 | backingStoreXML := volumeXML.BackingStore() 80 | backingStoreXML.SetPath(sourcePath) 81 | backingStoreXML.Format().SetType(targetXML.Format().Type()) 82 | backingStoreXML.RemoveTimestamps() 83 | } 84 | 85 | // switch to a format that supports backing store 86 | switch targetXML.Format().Type() { 87 | case "raw": 88 | targetXML.Format().SetType("qcow2") 89 | } 90 | } 91 | 92 | xmlString, err := volumeXML.MarshalToXML() 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | storageVol, err := pool.StorageVolCreateXML(xmlString, libvirt.StorageVolCreateFlags(0)) 98 | if err != nil { 99 | return nil, errors.Wrapf(err, "libvirt: failed to clone storage volume '%s' to '%s'", params.BackingVolumeName, params.Name) 100 | } 101 | defer storageVol.Free() 102 | 103 | return getStorageVolumeXML(storageVol) 104 | } 105 | 106 | func createStorageVolumeFromContent(connect *libvirt.Connect, pool *libvirt.StoragePool, params CreateStorageVolumeParams) (*libvirtxml.StorageVolume, error) { 107 | storageVol, err := createEmptyStorageVolume(pool, params.Name, params.CapacityGiB) 108 | if err != nil { 109 | return nil, err 110 | } 111 | defer storageVol.Free() 112 | 113 | stream, err := connect.NewStream(libvirt.STREAM_NONBLOCK) 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer stream.Free() 118 | 119 | if err := storageVol.Upload(stream, 0, 0, libvirt.StorageVolUploadFlags(0)); err != nil { 120 | return nil, err 121 | } 122 | 123 | if err := streamSendAll(stream, params.Content); err != nil { 124 | return nil, errors.Wrap(err, "libvirt: failed to upload storage volume content") 125 | } 126 | 127 | return getStorageVolumeXML(storageVol) 128 | } 129 | 130 | func createEmptyStorageVolume(pool *libvirt.StoragePool, name string, capacityGiB uint) (*libvirt.StorageVol, error) { 131 | volumeXML := libvirtxml.NewStorageVolume() 132 | volumeXML.SetType("file") 133 | volumeXML.SetName(name) 134 | volumeXML.Target().Format().SetType("qcow2") 135 | 136 | volumeXML.Capacity().SetUnit("GiB") 137 | volumeXML.Capacity().SetValue(uint64(capacityGiB)) 138 | volumeXML.Allocation().SetUnit("bytes") 139 | volumeXML.Allocation().SetValue(0) 140 | 141 | xmlString, err := volumeXML.MarshalToXML() 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | storageVol, err := pool.StorageVolCreateXML(xmlString, libvirt.StorageVolCreateFlags(0)) 147 | if err != nil { 148 | return nil, errors.Wrapf(err, "libvirt: failed to create empty storage volume '%s'", name) 149 | } 150 | 151 | return storageVol, nil 152 | } 153 | -------------------------------------------------------------------------------- /libvirtxml/capabilities.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type Capabilities struct { 4 | doc *Document 5 | root *Node 6 | } 7 | 8 | func NewCapabilitiesForXML(xmlDoc string) (*Capabilities, error) { 9 | doc := &Document{} 10 | if err := doc.Unmarshal(xmlDoc); err != nil { 11 | return nil, err 12 | } 13 | 14 | if doc.Root == nil { 15 | doc.Root = NewNode(nameForLocal("capabilities")) 16 | } 17 | 18 | return &Capabilities{ 19 | doc: doc, 20 | root: doc.Root, 21 | }, nil 22 | } 23 | 24 | func (s Capabilities) Host() CapabilitiesHost { 25 | node := s.root.ensureNode(nameForLocal("host")) 26 | return newCapabilitiesHost(node) 27 | } 28 | 29 | func (s Capabilities) Guests() []CapabilitiesGuest { 30 | var result []CapabilitiesGuest 31 | 32 | nodes := s.root.findNodes(nameForLocal("guest")) 33 | for _, node := range nodes { 34 | result = append(result, newCapabilitiesGuest(node)) 35 | } 36 | 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /libvirtxml/capabilitiesGust.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type CapabilitiesGuest struct { 4 | node *Node 5 | } 6 | 7 | func newCapabilitiesGuest(node *Node) CapabilitiesGuest { 8 | return CapabilitiesGuest{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s CapabilitiesGuest) OSType() string { 14 | return s.node.ensureNode(nameForLocal("os_type")).CharData 15 | } 16 | 17 | func (s CapabilitiesGuest) Arch() CapabilitiesGustArch { 18 | node := s.node.ensureNode(nameForLocal("arch")) 19 | return newCapabilitiesGustArch(node) 20 | } 21 | -------------------------------------------------------------------------------- /libvirtxml/capabilitiesGustArch.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type CapabilitiesGustArch struct { 4 | node *Node 5 | } 6 | 7 | func newCapabilitiesGustArch(node *Node) CapabilitiesGustArch { 8 | return CapabilitiesGustArch{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s CapabilitiesGustArch) Name() string { 14 | return s.node.getAttribute(nameForLocal("name")) 15 | } 16 | 17 | func (s CapabilitiesGustArch) Emulator() string { 18 | return s.node.ensureNode(nameForLocal("emulator")).CharData 19 | } 20 | -------------------------------------------------------------------------------- /libvirtxml/capabilitiesHost.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type CapabilitiesHost struct { 4 | node *Node 5 | } 6 | 7 | func newCapabilitiesHost(node *Node) CapabilitiesHost { 8 | return CapabilitiesHost{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s CapabilitiesHost) UUID() string { 14 | return s.node.ensureNode(nameForLocal("uuid")).CharData 15 | } 16 | 17 | func (s CapabilitiesHost) CPU() CapabilitiesHostCPU { 18 | node := s.node.ensureNode(nameForLocal("cpu")) 19 | return newCapabilitiesHostCPU(node) 20 | } 21 | -------------------------------------------------------------------------------- /libvirtxml/capabilitiesHostCpu.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type CapabilitiesHostCPU struct { 4 | node *Node 5 | } 6 | 7 | func newCapabilitiesHostCPU(node *Node) CapabilitiesHostCPU { 8 | return CapabilitiesHostCPU{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s CapabilitiesHostCPU) Arch() string { 14 | return s.node.ensureNode(nameForLocal("arch")).CharData 15 | } 16 | -------------------------------------------------------------------------------- /libvirtxml/document.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "errors" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | type Document struct { 12 | ProcInst *xml.ProcInst 13 | Root *Node 14 | CharData string 15 | Comments string 16 | } 17 | 18 | func (d *Document) Unmarshal(xmlDoc string) error { 19 | reader := strings.NewReader(xmlDoc) 20 | decoder := xml.NewDecoder(reader) 21 | 22 | nodes, charData, comments, procInst, err := decodeNodes(decoder) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if len(nodes) == 0 { 28 | return errors.New("libvirtxml: invalid XML document - no root element") 29 | } 30 | 31 | if len(nodes) > 1 { 32 | return errors.New("libvirtxml: invalid XML document - conatains multiple root elements") 33 | } 34 | 35 | d.ProcInst = procInst 36 | d.Root = nodes[0] 37 | d.CharData = charData 38 | d.Comments = comments 39 | 40 | return nil 41 | } 42 | 43 | func (d *Document) Marshal() (string, error) { 44 | var buffer bytes.Buffer 45 | 46 | encoder := xml.NewEncoder(&buffer) 47 | encoder.Indent("", " ") 48 | 49 | if d.ProcInst != nil { 50 | if err := encoder.EncodeToken(d.ProcInst); err != nil { 51 | return "", err 52 | } 53 | } 54 | 55 | if d.Comments != "" { 56 | if err := encoder.EncodeToken(xml.Comment(d.Comments)); err != nil { 57 | return "", err 58 | } 59 | } 60 | 61 | if d.CharData != "" { 62 | if err := encoder.EncodeToken(xml.CharData(d.CharData)); err != nil { 63 | return "", err 64 | } 65 | } 66 | 67 | if err := encodeNode(encoder, d.Root); err != nil { 68 | return "", err 69 | } 70 | 71 | encoder.Flush() 72 | return buffer.String(), nil 73 | } 74 | 75 | func decodeNodes(decoder *xml.Decoder) ([]*Node, string, string, *xml.ProcInst, error) { 76 | var nodes []*Node 77 | var charData string 78 | var comments string 79 | var procInst *xml.ProcInst 80 | var err error 81 | 82 | loop: 83 | for { 84 | var token xml.Token 85 | token, err = decoder.Token() 86 | if err != nil { 87 | break 88 | } 89 | 90 | switch t := token.(type) { 91 | case xml.StartElement: 92 | var node *Node 93 | node, err = decodeNode(decoder, t) 94 | if err != nil { 95 | break loop 96 | } 97 | 98 | nodes = append(nodes, node) 99 | case xml.EndElement: 100 | break loop 101 | case xml.CharData: 102 | str := string(t) 103 | str = strings.TrimSpace(str) 104 | if str != "" { 105 | charData = strings.Join([]string{charData, str}, "") 106 | } 107 | case xml.Comment: 108 | comments = strings.Join([]string{comments, string(t)}, "") 109 | case xml.ProcInst: 110 | procInst = &t 111 | } 112 | } 113 | 114 | if err != nil && err != io.EOF { 115 | return nil, "", "", nil, err 116 | } 117 | 118 | return nodes, charData, comments, procInst, nil 119 | } 120 | 121 | func decodeNode(decoder *xml.Decoder, element xml.StartElement) (*Node, error) { 122 | attributes := make([]*Attribute, len(element.Attr), len(element.Attr)) 123 | 124 | for i, attr := range element.Attr { 125 | attributes[i] = &Attribute{ 126 | Name: nameForXMLName(attr.Name), 127 | Value: attr.Value, 128 | } 129 | } 130 | 131 | result := NewNode(nameForXMLName(element.Name)) 132 | result.Attributes = attributes 133 | 134 | nodes, charData, comments, _, err := decodeNodes(decoder) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | result.Nodes = nodes 140 | result.CharData = charData 141 | result.Comments = comments 142 | 143 | return result, nil 144 | } 145 | 146 | func encodeNode(encoder *xml.Encoder, node *Node) error { 147 | startElement := xml.StartElement{ 148 | Name: node.Name.toXMLName(), 149 | } 150 | 151 | for _, attribute := range node.Attributes { 152 | if attribute.Value == "" { 153 | continue 154 | } 155 | 156 | startElement.Attr = append(startElement.Attr, xml.Attr{ 157 | Name: attribute.Name.toXMLName(), 158 | Value: attribute.Value, 159 | }) 160 | } 161 | 162 | if err := encoder.EncodeToken(startElement); err != nil { 163 | return err 164 | } 165 | 166 | if node.Comments != "" { 167 | if err := encoder.EncodeToken(xml.Comment(node.Comments)); err != nil { 168 | return err 169 | } 170 | } 171 | 172 | if node.CharData != "" { 173 | if err := encoder.EncodeToken(xml.CharData(node.CharData)); err != nil { 174 | return err 175 | } 176 | } 177 | 178 | for _, node := range node.Nodes { 179 | if err := encodeNode(encoder, node); err != nil { 180 | return err 181 | } 182 | } 183 | 184 | endElement := xml.EndElement{ 185 | Name: node.Name.toXMLName(), 186 | } 187 | 188 | if err := encoder.EncodeToken(endElement); err != nil { 189 | return err 190 | } 191 | 192 | return nil 193 | } 194 | -------------------------------------------------------------------------------- /libvirtxml/domain.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type Domain struct { 4 | doc *Document 5 | root *Node 6 | } 7 | 8 | func NewDomainForXML(xmlDoc string) (*Domain, error) { 9 | doc := &Document{} 10 | if err := doc.Unmarshal(xmlDoc); err != nil { 11 | return nil, err 12 | } 13 | 14 | if doc.Root == nil { 15 | doc.Root = NewNode(nameForLocal("domain")) 16 | } 17 | 18 | return &Domain{ 19 | doc: doc, 20 | root: doc.Root, 21 | }, nil 22 | } 23 | 24 | func (s Domain) MarshalToXML() (string, error) { 25 | return s.doc.Marshal() 26 | } 27 | 28 | func (s Domain) Name() string { 29 | return s.root.ensureNode(nameForLocal("name")).CharData 30 | } 31 | 32 | func (s Domain) SetName(value string) { 33 | s.root.ensureNode(nameForLocal("name")).CharData = value 34 | } 35 | 36 | func (s Domain) UUID() string { 37 | return s.root.ensureNode(nameForLocal("uuid")).CharData 38 | } 39 | 40 | func (s Domain) SetUUID(value string) { 41 | s.root.ensureNode(nameForLocal("uuid")).CharData = value 42 | } 43 | 44 | func (s Domain) ID() string { 45 | return s.root.getAttribute(nameForLocal("id")) 46 | } 47 | 48 | func (s Domain) SetID(value string) { 49 | s.root.setAttribute(nameForLocal("id"), value) 50 | } 51 | 52 | func (s Domain) Devices() DomainDevices { 53 | node := s.root.ensureNode(nameForLocal("devices")) 54 | return newDomainDevices(node) 55 | } 56 | 57 | func (s Domain) VCPU() DomainVCPU { 58 | node := s.root.ensureNode(nameForLocal("vcpu")) 59 | return newDomainVCPU(node) 60 | } 61 | 62 | func (s Domain) Memory() DomainMemory { 63 | node := s.root.ensureNode(nameForLocal("memory")) 64 | return newDomainMemory(node) 65 | } 66 | 67 | func (s Domain) Metadata() Metadata { 68 | node := s.root.ensureNode(nameForLocal("metadata")) 69 | return newMetadata(node) 70 | } 71 | -------------------------------------------------------------------------------- /libvirtxml/domainChannel.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainChannel struct { 4 | node *Node 5 | } 6 | 7 | func newDomainChannel(node *Node) DomainChannel { 8 | return DomainChannel{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainChannel) Type() string { 14 | return s.node.getAttribute(nameForLocal("type")) 15 | } 16 | 17 | func (s DomainChannel) SetType(value string) { 18 | s.node.setAttribute(nameForLocal("type"), value) 19 | } 20 | 21 | func (s DomainChannel) SourcePath() string { 22 | node := s.node.ensureNode(nameForLocal("source")) 23 | return node.getAttribute(nameForLocal("path")) 24 | } 25 | 26 | func (s DomainChannel) SetSourcePath(value string) { 27 | node := s.node.ensureNode(nameForLocal("source")) 28 | node.setAttribute(nameForLocal("path"), value) 29 | } 30 | -------------------------------------------------------------------------------- /libvirtxml/domainDevices.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainDevices struct { 4 | node *Node 5 | } 6 | 7 | func newDomainDevices(node *Node) DomainDevices { 8 | return DomainDevices{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainDevices) Emulator() string { 14 | return s.node.ensureNode(nameForLocal("emulator")).CharData 15 | } 16 | 17 | func (s DomainDevices) SetEmulator(value string) { 18 | s.node.ensureNode(nameForLocal("emulator")).CharData = value 19 | } 20 | 21 | func (s DomainDevices) Disks() []DomainDisk { 22 | var result []DomainDisk 23 | 24 | nodes := s.node.findNodes(nameForLocal("disk")) 25 | for _, node := range nodes { 26 | result = append(result, newDomainDisk(node)) 27 | } 28 | 29 | return result 30 | } 31 | 32 | func (s DomainDevices) NewDisk() DomainDisk { 33 | node := NewNode(nameForLocal("disk")) 34 | s.node.addNode(node) 35 | return newDomainDisk(node) 36 | } 37 | 38 | func (s DomainDevices) SetDisks(disks []DomainDisk) { 39 | s.node.removeNodes(nameForLocal("disk")) 40 | 41 | for _, disk := range disks { 42 | s.node.addNode(disk.node) 43 | } 44 | } 45 | 46 | func (s DomainDevices) Graphics() []DomainGraphic { 47 | var result []DomainGraphic 48 | 49 | nodes := s.node.findNodes(nameForLocal("graphics")) 50 | for _, node := range nodes { 51 | result = append(result, newDomainGraphic(node)) 52 | } 53 | 54 | return result 55 | } 56 | 57 | func (s DomainDevices) Interfaces() []DomainInterface { 58 | var result []DomainInterface 59 | 60 | nodes := s.node.findNodes(nameForLocal("interface")) 61 | for _, node := range nodes { 62 | result = append(result, newDomainInterface(node)) 63 | } 64 | 65 | return result 66 | } 67 | 68 | func (s DomainDevices) NewInterface() DomainInterface { 69 | node := NewNode(nameForLocal("interface")) 70 | s.node.addNode(node) 71 | return newDomainInterface(node) 72 | } 73 | 74 | func (s DomainDevices) Channels() []DomainChannel { 75 | var result []DomainChannel 76 | 77 | nodes := s.node.findNodes(nameForLocal("channel")) 78 | for _, node := range nodes { 79 | result = append(result, newDomainChannel(node)) 80 | } 81 | 82 | return result 83 | } 84 | 85 | func (s DomainDevices) Filesystems() []DomainFilesystem { 86 | var result []DomainFilesystem 87 | 88 | nodes := s.node.findNodes(nameForLocal("filesystem")) 89 | for _, node := range nodes { 90 | result = append(result, newDomainFilesystem(node)) 91 | } 92 | 93 | return result 94 | } 95 | 96 | func (s DomainDevices) NewFilesystem() DomainFilesystem { 97 | node := NewNode(nameForLocal("filesystem")) 98 | s.node.addNode(node) 99 | return newDomainFilesystem(node) 100 | } 101 | -------------------------------------------------------------------------------- /libvirtxml/domainDisk.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainDisk struct { 4 | node *Node 5 | } 6 | 7 | func newDomainDisk(node *Node) DomainDisk { 8 | return DomainDisk{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainDisk) Type() string { 14 | return s.node.getAttribute(nameForLocal("type")) 15 | } 16 | 17 | func (s DomainDisk) SetType(value string) { 18 | s.node.setAttribute(nameForLocal("type"), value) 19 | } 20 | 21 | func (s DomainDisk) Device() string { 22 | return s.node.getAttribute(nameForLocal("device")) 23 | } 24 | 25 | func (s DomainDisk) SetDevice(value string) { 26 | s.node.setAttribute(nameForLocal("device"), value) 27 | } 28 | 29 | func (s DomainDisk) Readonly() bool { 30 | return s.node.hasNode(nameForLocal("readonly")) 31 | } 32 | 33 | func (s DomainDisk) SetReadonly(value bool) { 34 | if value { 35 | s.node.ensureNode(nameForLocal("readonly")) 36 | } else { 37 | s.node.removeNodes(nameForLocal("readonly")) 38 | } 39 | } 40 | 41 | func (s DomainDisk) Shareable() bool { 42 | return s.node.hasNode(nameForLocal("shareable")) 43 | } 44 | 45 | func (s DomainDisk) SetShareable(value bool) { 46 | if value { 47 | s.node.ensureNode(nameForLocal("shareable")) 48 | } else { 49 | s.node.removeNodes(nameForLocal("shareable")) 50 | } 51 | } 52 | 53 | func (s DomainDisk) Source() DomainDiskSource { 54 | node := s.node.ensureNode(nameForLocal("source")) 55 | return newDomainDiskSource(node) 56 | } 57 | 58 | func (s DomainDisk) Target() DomainDiskTarget { 59 | node := s.node.ensureNode(nameForLocal("target")) 60 | return newDomainDiskTarget(node) 61 | } 62 | 63 | func (s DomainDisk) Driver() DomainDiskDriver { 64 | node := s.node.ensureNode(nameForLocal("driver")) 65 | return newDomainDiskDriver(node) 66 | } 67 | -------------------------------------------------------------------------------- /libvirtxml/domainDiskDriver.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainDiskDriver struct { 4 | node *Node 5 | } 6 | 7 | func newDomainDiskDriver(node *Node) DomainDiskDriver { 8 | return DomainDiskDriver{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainDiskDriver) Name() string { 14 | return s.node.getAttribute(nameForLocal("name")) 15 | } 16 | 17 | func (s DomainDiskDriver) SetName(value string) { 18 | s.node.setAttribute(nameForLocal("name"), value) 19 | } 20 | 21 | func (s DomainDiskDriver) Type() string { 22 | return s.node.getAttribute(nameForLocal("type")) 23 | } 24 | 25 | func (s DomainDiskDriver) SetType(value string) { 26 | s.node.setAttribute(nameForLocal("type"), value) 27 | } 28 | -------------------------------------------------------------------------------- /libvirtxml/domainDiskSource.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainDiskSource struct { 4 | node *Node 5 | } 6 | 7 | func newDomainDiskSource(node *Node) DomainDiskSource { 8 | return DomainDiskSource{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainDiskSource) File() string { 14 | return s.node.getAttribute(nameForLocal("file")) 15 | } 16 | 17 | func (s DomainDiskSource) SetFile(value string) { 18 | s.node.setAttribute(nameForLocal("file"), value) 19 | } 20 | 21 | func (s DomainDiskSource) Pool() string { 22 | return s.node.getAttribute(nameForLocal("pool")) 23 | } 24 | 25 | func (s DomainDiskSource) SetPool(value string) { 26 | s.node.setAttribute(nameForLocal("pool"), value) 27 | } 28 | 29 | func (s DomainDiskSource) Volume() string { 30 | return s.node.getAttribute(nameForLocal("volume")) 31 | } 32 | 33 | func (s DomainDiskSource) SetVolume(value string) { 34 | s.node.setAttribute(nameForLocal("volume"), value) 35 | } 36 | 37 | func (s DomainDiskSource) Mode() string { 38 | return s.node.getAttribute(nameForLocal("mode")) 39 | } 40 | 41 | func (s DomainDiskSource) SetMode(value string) { 42 | s.node.setAttribute(nameForLocal("mode"), value) 43 | } 44 | -------------------------------------------------------------------------------- /libvirtxml/domainDiskTarget.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainDiskTarget struct { 4 | node *Node 5 | } 6 | 7 | func newDomainDiskTarget(node *Node) DomainDiskTarget { 8 | return DomainDiskTarget{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainDiskTarget) Dev() string { 14 | return s.node.getAttribute(nameForLocal("dev")) 15 | } 16 | 17 | func (s DomainDiskTarget) SetDev(value string) { 18 | s.node.setAttribute(nameForLocal("dev"), value) 19 | } 20 | 21 | func (s DomainDiskTarget) Bus() string { 22 | return s.node.getAttribute(nameForLocal("bus")) 23 | } 24 | 25 | func (s DomainDiskTarget) SetBus(value string) { 26 | s.node.setAttribute(nameForLocal("bus"), value) 27 | } 28 | -------------------------------------------------------------------------------- /libvirtxml/domainFilesystem.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainFilesystem struct { 4 | node *Node 5 | } 6 | 7 | func newDomainFilesystem(node *Node) DomainFilesystem { 8 | return DomainFilesystem{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainFilesystem) Type() string { 14 | return s.node.getAttribute(nameForLocal("type")) 15 | } 16 | 17 | func (s DomainFilesystem) SetType(value string) { 18 | s.node.setAttribute(nameForLocal("type"), value) 19 | } 20 | 21 | func (s DomainFilesystem) Accessmode() string { 22 | return s.node.getAttribute(nameForLocal("accessmode")) 23 | } 24 | 25 | func (s DomainFilesystem) SetAccessmode(value string) { 26 | s.node.setAttribute(nameForLocal("accessmode"), value) 27 | } 28 | 29 | func (s DomainFilesystem) SourceDir() string { 30 | node := s.node.ensureNode(nameForLocal("source")) 31 | return node.getAttribute(nameForLocal("dir")) 32 | } 33 | 34 | func (s DomainFilesystem) SetSourceDir(value string) { 35 | node := s.node.ensureNode(nameForLocal("source")) 36 | node.setAttribute(nameForLocal("dir"), value) 37 | } 38 | 39 | func (s DomainFilesystem) TargetDir() string { 40 | node := s.node.ensureNode(nameForLocal("target")) 41 | return node.getAttribute(nameForLocal("dir")) 42 | } 43 | 44 | func (s DomainFilesystem) SetTargetDir(value string) { 45 | node := s.node.ensureNode(nameForLocal("target")) 46 | node.setAttribute(nameForLocal("dir"), value) 47 | } 48 | 49 | func (s DomainFilesystem) Readonly() bool { 50 | return s.node.hasNode(nameForLocal("readonly")) 51 | } 52 | 53 | func (s DomainFilesystem) SetReadonly(value bool) { 54 | if value { 55 | s.node.ensureNode(nameForLocal("readonly")) 56 | } else { 57 | s.node.removeNodes(nameForLocal("readonly")) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /libvirtxml/domainGraphic.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/golang/glog" 7 | ) 8 | 9 | type DomainGraphic struct { 10 | node *Node 11 | } 12 | 13 | func newDomainGraphic(node *Node) DomainGraphic { 14 | return DomainGraphic{ 15 | node: node, 16 | } 17 | } 18 | 19 | func (s DomainGraphic) Port() int { 20 | str := s.node.getAttribute(nameForLocal("port")) 21 | if str == "" { 22 | return -1 23 | } 24 | 25 | port, err := strconv.Atoi(str) 26 | if err != nil { 27 | port = 0 28 | glog.Warningf("libvirtxml: ignoring invalid domain graphics port '%s'", str) 29 | } 30 | return port 31 | } 32 | 33 | func (s DomainGraphic) SetPort(value int) { 34 | str := strconv.FormatInt(int64(value), 10) 35 | s.node.setAttribute(nameForLocal("port"), str) 36 | } 37 | -------------------------------------------------------------------------------- /libvirtxml/domainInterface.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainInterface struct { 4 | node *Node 5 | } 6 | 7 | func newDomainInterface(node *Node) DomainInterface { 8 | return DomainInterface{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainInterface) Type() string { 14 | return s.node.getAttribute(nameForLocal("type")) 15 | } 16 | 17 | func (s DomainInterface) SetType(value string) { 18 | s.node.setAttribute(nameForLocal("type"), value) 19 | } 20 | 21 | func (s DomainInterface) TargetDevice() string { 22 | node := s.node.ensureNode(nameForLocal("target")) 23 | return node.getAttribute(nameForLocal("dev")) 24 | } 25 | 26 | func (s DomainInterface) SetTargetDevice(value string) { 27 | node := s.node.ensureNode(nameForLocal("target")) 28 | node.setAttribute(nameForLocal("dev"), value) 29 | } 30 | 31 | func (s DomainInterface) MACAddress() string { 32 | node := s.node.ensureNode(nameForLocal("mac")) 33 | return node.getAttribute(nameForLocal("address")) 34 | } 35 | 36 | func (s DomainInterface) SetMACAddress(value string) { 37 | node := s.node.ensureNode(nameForLocal("mac")) 38 | node.setAttribute(nameForLocal("address"), value) 39 | } 40 | 41 | func (s DomainInterface) Source() DomainInterfaceSource { 42 | node := s.node.ensureNode(nameForLocal("source")) 43 | return newDomainInterfaceSource(node) 44 | } 45 | 46 | func (s DomainInterface) ModelType() string { 47 | node := s.node.ensureNode(nameForLocal("model")) 48 | return node.getAttribute(nameForLocal("type")) 49 | } 50 | 51 | func (s DomainInterface) SetModelType(value string) { 52 | node := s.node.ensureNode(nameForLocal("model")) 53 | node.setAttribute(nameForLocal("type"), value) 54 | } 55 | -------------------------------------------------------------------------------- /libvirtxml/domainInterfaceSource.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type DomainInterfaceSource struct { 4 | node *Node 5 | } 6 | 7 | func newDomainInterfaceSource(node *Node) DomainInterfaceSource { 8 | return DomainInterfaceSource{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s DomainInterfaceSource) Network() string { 14 | return s.node.getAttribute(nameForLocal("network")) 15 | } 16 | 17 | func (s DomainInterfaceSource) SetNetwork(value string) { 18 | s.node.setAttribute(nameForLocal("network"), value) 19 | } 20 | -------------------------------------------------------------------------------- /libvirtxml/domainMemory.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import "strconv" 4 | 5 | type DomainMemory struct { 6 | node *Node 7 | } 8 | 9 | func newDomainMemory(node *Node) DomainMemory { 10 | return DomainMemory{ 11 | node: node, 12 | } 13 | } 14 | 15 | func (s DomainMemory) Unit() string { 16 | return s.node.getAttribute(nameForLocal("unit")) 17 | } 18 | 19 | func (s DomainMemory) SetUnit(value string) { 20 | s.node.setAttribute(nameForLocal("unit"), value) 21 | } 22 | 23 | func (s DomainMemory) Value() string { 24 | return s.node.CharData 25 | } 26 | 27 | func (s DomainMemory) SetValue(value uint64) { 28 | s.node.CharData = strconv.FormatUint(value, 10) 29 | } 30 | -------------------------------------------------------------------------------- /libvirtxml/domainVcpu.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/golang/glog" 7 | ) 8 | 9 | type DomainVCPU struct { 10 | node *Node 11 | } 12 | 13 | func newDomainVCPU(node *Node) DomainVCPU { 14 | return DomainVCPU{ 15 | node: node, 16 | } 17 | } 18 | 19 | func (s DomainVCPU) Value() uint { 20 | str := s.node.CharData 21 | if str == "" { 22 | return 0 23 | } 24 | 25 | result, err := strconv.Atoi(str) 26 | if err != nil || result < 0 { 27 | result = 0 28 | glog.Warningf("libvirtxml: ignoring invalid vcpu value '%s'", str) 29 | } 30 | 31 | return uint(result) 32 | } 33 | 34 | func (s DomainVCPU) SetValue(value uint) { 35 | s.node.CharData = strconv.FormatUint(uint64(value), 10) 36 | } 37 | -------------------------------------------------------------------------------- /libvirtxml/metadata.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type Metadata struct { 4 | node *Node 5 | } 6 | 7 | func newMetadata(node *Node) Metadata { 8 | return Metadata{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (d Metadata) FindNodes(name Name) []*Node { 14 | return d.node.findNodes(name) 15 | } 16 | 17 | func (s Metadata) NewNode(name Name) *Node { 18 | node := NewNode(name) 19 | s.node.addNode(node) 20 | return node 21 | } 22 | -------------------------------------------------------------------------------- /libvirtxml/name.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import "encoding/xml" 4 | 5 | type Name struct { 6 | Local string 7 | Space string 8 | } 9 | 10 | func NewName(namespace string, local string) Name { 11 | return Name{ 12 | Local: local, 13 | Space: namespace, 14 | } 15 | } 16 | 17 | func nameForLocal(local string) Name { 18 | return Name{ 19 | Local: local, 20 | } 21 | } 22 | 23 | func nameForXMLName(name xml.Name) Name { 24 | return Name{ 25 | Local: name.Local, 26 | Space: name.Space, 27 | } 28 | } 29 | 30 | func (n Name) toXMLName() xml.Name { 31 | return xml.Name{ 32 | Local: n.Local, 33 | Space: n.Space, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libvirtxml/network.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type Network struct { 4 | doc *Document 5 | root *Node 6 | } 7 | 8 | func NewNetwork() Network { 9 | doc := &Document{} 10 | doc.Root = NewNode(nameForLocal("network")) 11 | 12 | return Network{ 13 | doc: doc, 14 | root: doc.Root, 15 | } 16 | } 17 | 18 | func NewNetworkForXML(xmlDoc string) (*Network, error) { 19 | doc := &Document{} 20 | if err := doc.Unmarshal(xmlDoc); err != nil { 21 | return nil, err 22 | } 23 | 24 | if doc.Root == nil { 25 | doc.Root = NewNode(nameForLocal("network")) 26 | } 27 | 28 | return &Network{ 29 | doc: doc, 30 | root: doc.Root, 31 | }, nil 32 | } 33 | 34 | func (s Network) MarshalToXML() (string, error) { 35 | return s.doc.Marshal() 36 | } 37 | 38 | func (s Network) Name() string { 39 | return s.root.ensureNode(nameForLocal("name")).CharData 40 | } 41 | 42 | func (s Network) SetName(value string) { 43 | s.root.ensureNode(nameForLocal("name")).CharData = value 44 | } 45 | 46 | func (s Network) UUID() string { 47 | return s.root.ensureNode(nameForLocal("uuid")).CharData 48 | } 49 | 50 | func (s Network) SetUUID(value string) { 51 | s.root.ensureNode(nameForLocal("uuid")).CharData = value 52 | } 53 | 54 | func (s Network) MACAddress() string { 55 | node := s.root.ensureNode(nameForLocal("mac")) 56 | return node.getAttribute(nameForLocal("address")) 57 | } 58 | 59 | func (s Network) Forward() NetworkForward { 60 | node := s.root.ensureNode(nameForLocal("forward")) 61 | return newNetworkForward(node) 62 | } 63 | 64 | func (s Network) Bridge() NetworkBridge { 65 | node := s.root.ensureNode(nameForLocal("bridge")) 66 | return newNetworkBridge(node) 67 | } 68 | 69 | func (s Network) IPs() []NetworkIP { 70 | var result []NetworkIP 71 | 72 | nodes := s.root.findNodes(nameForLocal("ip")) 73 | for _, node := range nodes { 74 | result = append(result, newNetworkIP(node)) 75 | } 76 | 77 | return result 78 | } 79 | 80 | func (s Network) SetIPs(ips []NetworkIP) { 81 | s.root.removeNodes(nameForLocal("ip")) 82 | 83 | for _, ip := range ips { 84 | s.root.addNode(ip.node) 85 | } 86 | } 87 | 88 | func (s Network) NewIP() NetworkIP { 89 | node := NewNode(nameForLocal("ip")) 90 | s.root.addNode(node) 91 | return newNetworkIP(node) 92 | } 93 | 94 | func (s Network) Metadata() Metadata { 95 | node := s.root.ensureNode(nameForLocal("metadata")) 96 | return newMetadata(node) 97 | } 98 | 99 | func (s Network) Domain() NetworDomain { 100 | node := s.root.ensureNode(nameForLocal("domain")) 101 | return newNetworkDomain(node) 102 | } 103 | 104 | func (s Network) DNS() NetworkDNS { 105 | node := s.root.ensureNode(nameForLocal("dns")) 106 | return newNetworkDNS(node) 107 | } 108 | -------------------------------------------------------------------------------- /libvirtxml/networkBridge.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type NetworkBridge struct { 4 | node *Node 5 | } 6 | 7 | func newNetworkBridge(node *Node) NetworkBridge { 8 | return NetworkBridge{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s NetworkBridge) Name() string { 14 | return s.node.getAttribute(nameForLocal("name")) 15 | } 16 | 17 | func (s NetworkBridge) SetName(value string) { 18 | s.node.setAttribute(nameForLocal("name"), value) 19 | } 20 | 21 | func (s NetworkBridge) STP() bool { 22 | stp := s.node.getAttribute(nameForLocal("stp")) 23 | return stp == "on" 24 | } 25 | 26 | func (s NetworkBridge) SetSTP(value bool) { 27 | if value { 28 | s.node.setAttribute(nameForLocal("stp"), "on") 29 | } else { 30 | s.node.setAttribute(nameForLocal("stp"), "off") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libvirtxml/networkDns.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type NetworkDNS struct { 4 | node *Node 5 | } 6 | 7 | func newNetworkDNS(node *Node) NetworkDNS { 8 | return NetworkDNS{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s NetworkDNS) Enable() bool { 14 | stp := s.node.getAttribute(nameForLocal("enable")) 15 | return stp == "yes" 16 | } 17 | 18 | func (s NetworkDNS) SetEnable(value bool) { 19 | if value { 20 | s.node.setAttribute(nameForLocal("enable"), "yes") 21 | } else { 22 | s.node.setAttribute(nameForLocal("enable"), "no") 23 | } 24 | } 25 | 26 | func (s NetworkDNS) ForwardPlainNames() bool { 27 | stp := s.node.getAttribute(nameForLocal("forwardPlainNames")) 28 | return stp == "yes" 29 | } 30 | 31 | func (s NetworkDNS) SetForwardPlainNames(value bool) { 32 | if value { 33 | s.node.setAttribute(nameForLocal("forwardPlainNames"), "yes") 34 | } else { 35 | s.node.setAttribute(nameForLocal("forwardPlainNames"), "no") 36 | } 37 | } 38 | 39 | func (s Network) Hosts() []NetworkDNSHost { 40 | var result []NetworkDNSHost 41 | 42 | nodes := s.root.findNodes(nameForLocal("host")) 43 | for _, node := range nodes { 44 | result = append(result, newNetworkDNSHost(node)) 45 | } 46 | 47 | return result 48 | } 49 | -------------------------------------------------------------------------------- /libvirtxml/networkDnsHost.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type NetworkDNSHost struct { 4 | node *Node 5 | } 6 | 7 | func newNetworkDNSHost(node *Node) NetworkDNSHost { 8 | return NetworkDNSHost{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s NetworkDNSHost) IP() string { 14 | return s.node.getAttribute(nameForLocal("ip")) 15 | } 16 | 17 | func (s NetworkDNSHost) SetEnable(value string) { 18 | s.node.setAttribute(nameForLocal("ip"), value) 19 | } 20 | 21 | func (s Network) Hostnames() []string { 22 | var result []string 23 | 24 | nodes := s.root.findNodes(nameForLocal("hostname")) 25 | for _, node := range nodes { 26 | result = append(result, node.CharData) 27 | } 28 | 29 | return result 30 | } 31 | 32 | func (s Network) SetHostnames(values []string) { 33 | name := nameForLocal("hostname") 34 | s.root.removeNodes(name) 35 | 36 | for _, value := range values { 37 | node := NewNode(name) 38 | node.CharData = value 39 | s.root.addNode(node) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libvirtxml/networkDomain.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type NetworDomain struct { 4 | node *Node 5 | } 6 | 7 | func newNetworkDomain(node *Node) NetworDomain { 8 | return NetworDomain{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s NetworDomain) Name() string { 14 | return s.node.getAttribute(nameForLocal("name")) 15 | } 16 | 17 | func (s NetworDomain) SetName(value string) { 18 | s.node.setAttribute(nameForLocal("name"), value) 19 | } 20 | 21 | func (s NetworDomain) LocalOnly() bool { 22 | stp := s.node.getAttribute(nameForLocal("localOnly")) 23 | return stp == "yes" 24 | } 25 | 26 | func (s NetworDomain) SetLocalOnly(value bool) { 27 | if value { 28 | s.node.setAttribute(nameForLocal("localOnly"), "yes") 29 | } else { 30 | s.node.setAttribute(nameForLocal("localOnly"), "no") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libvirtxml/networkForward.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import "strconv" 4 | 5 | type NetworkForward struct { 6 | node *Node 7 | } 8 | 9 | func newNetworkForward(node *Node) NetworkForward { 10 | return NetworkForward{ 11 | node: node, 12 | } 13 | } 14 | 15 | func (s NetworkForward) Mode() string { 16 | return s.node.getAttribute(nameForLocal("mode")) 17 | } 18 | 19 | func (s NetworkForward) SetMode(value string) { 20 | s.node.setAttribute(nameForLocal("mode"), value) 21 | } 22 | 23 | func (s NetworkForward) SetNATPortRange(start int, end int) { 24 | nat := s.node.ensureNode(nameForLocal("nat")) 25 | port := nat.ensureNode(nameForLocal("port")) 26 | 27 | startStr := strconv.FormatInt(int64(start), 10) 28 | endStr := strconv.FormatInt(int64(end), 10) 29 | 30 | port.setAttribute(nameForLocal("start"), startStr) 31 | port.setAttribute(nameForLocal("end"), endStr) 32 | } 33 | -------------------------------------------------------------------------------- /libvirtxml/networkIp.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/golang/glog" 7 | ) 8 | 9 | type NetworkIP struct { 10 | node *Node 11 | } 12 | 13 | func newNetworkIP(node *Node) NetworkIP { 14 | return NetworkIP{ 15 | node: node, 16 | } 17 | } 18 | 19 | func (s NetworkIP) Address() string { 20 | return s.node.getAttribute(nameForLocal("address")) 21 | } 22 | 23 | func (s NetworkIP) SetAddress(value string) { 24 | s.node.setAttribute(nameForLocal("address"), value) 25 | } 26 | 27 | func (s NetworkIP) Family() string { 28 | return s.node.getAttribute(nameForLocal("family")) 29 | } 30 | 31 | func (s NetworkIP) SetFamily(value string) { 32 | s.node.setAttribute(nameForLocal("family"), value) 33 | } 34 | 35 | func (s NetworkIP) Netmask() string { 36 | return s.node.getAttribute(nameForLocal("netmask")) 37 | } 38 | 39 | func (s NetworkIP) SetNetmask(value string) { 40 | s.node.setAttribute(nameForLocal("netmask"), value) 41 | } 42 | 43 | func (s NetworkIP) Prefix() int { 44 | str := s.node.getAttribute(nameForLocal("prefix")) 45 | if str == "" { 46 | return 0 47 | } 48 | 49 | prefix, err := strconv.Atoi(str) 50 | if err != nil { 51 | prefix = 0 52 | glog.Warningf("libvirtxml: ignoring invalid network IP prefix '%s'", str) 53 | } 54 | 55 | return prefix 56 | } 57 | 58 | func (s NetworkIP) SetPrefix(value int) { 59 | str := strconv.FormatInt(int64(value), 10) 60 | s.node.setAttribute(nameForLocal("prefix"), str) 61 | } 62 | 63 | func (s NetworkIP) SetDHCPRange(start string, end string) { 64 | dhcp := s.node.ensureNode(nameForLocal("dhcp")) 65 | rng := dhcp.ensureNode(nameForLocal("range")) 66 | 67 | rng.setAttribute(nameForLocal("start"), start) 68 | rng.setAttribute(nameForLocal("end"), end) 69 | } 70 | -------------------------------------------------------------------------------- /libvirtxml/node.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type Node struct { 4 | Name Name 5 | Attributes []*Attribute 6 | Nodes []*Node 7 | CharData string 8 | Comments string 9 | } 10 | 11 | type Attribute struct { 12 | Name Name 13 | Value string 14 | } 15 | 16 | func NewNode(name Name) *Node { 17 | return &Node{ 18 | Name: name, 19 | Attributes: make([]*Attribute, 0), 20 | Nodes: make([]*Node, 0), 21 | } 22 | } 23 | 24 | func (n *Node) findAttribute(name Name) *Attribute { 25 | for _, attr := range n.Attributes { 26 | if attr.Name == name { 27 | return attr 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (n *Node) findNodes(name Name) []*Node { 35 | var result []*Node 36 | 37 | for _, node := range n.Nodes { 38 | if node.Name == name { 39 | result = append(result, node) 40 | } 41 | } 42 | 43 | return result 44 | } 45 | 46 | func (n *Node) hasNode(name Name) bool { 47 | for _, node := range n.Nodes { 48 | if node.Name == name { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | 56 | func (n *Node) ensureNode(name Name) *Node { 57 | for _, node := range n.Nodes { 58 | if node.Name == name { 59 | return node 60 | } 61 | } 62 | 63 | newNode := NewNode(name) 64 | n.Nodes = append(n.Nodes, newNode) 65 | return newNode 66 | } 67 | 68 | func (n *Node) setAttribute(name Name, value string) { 69 | attr := n.findAttribute(name) 70 | if attr == nil { 71 | attr = &Attribute{ 72 | Name: name, 73 | } 74 | n.Attributes = append(n.Attributes, attr) 75 | } 76 | 77 | attr.Value = value 78 | } 79 | 80 | func (n *Node) getAttribute(name Name) string { 81 | attr := n.findAttribute(name) 82 | if attr != nil { 83 | return attr.Value 84 | } 85 | 86 | return "" 87 | } 88 | 89 | func (n *Node) removeAttribute(name Name) { 90 | var filtered []*Attribute 91 | 92 | for _, attr := range n.Attributes { 93 | if attr.Name != name { 94 | filtered = append(filtered, attr) 95 | } 96 | } 97 | 98 | n.Attributes = filtered 99 | } 100 | 101 | func (n *Node) removeNodes(name Name) { 102 | var filtered []*Node 103 | 104 | for _, node := range n.Nodes { 105 | if node.Name != name { 106 | filtered = append(filtered, node) 107 | } 108 | } 109 | 110 | n.Nodes = filtered 111 | } 112 | 113 | func (n *Node) addNode(node *Node) { 114 | n.Nodes = append(n.Nodes, node) 115 | } 116 | -------------------------------------------------------------------------------- /libvirtxml/storagePool.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StoragePool struct { 4 | doc *Document 5 | root *Node 6 | } 7 | 8 | func NewStoragePool() StoragePool { 9 | doc := &Document{} 10 | doc.Root = NewNode(nameForLocal("pool")) 11 | 12 | return StoragePool{ 13 | doc: doc, 14 | root: doc.Root, 15 | } 16 | } 17 | 18 | func NewStoragePoolForXML(xmlDoc string) (*StoragePool, error) { 19 | doc := &Document{} 20 | if err := doc.Unmarshal(xmlDoc); err != nil { 21 | return nil, err 22 | } 23 | 24 | if doc.Root == nil { 25 | doc.Root = NewNode(nameForLocal("pool")) 26 | } 27 | 28 | return &StoragePool{ 29 | doc: doc, 30 | root: doc.Root, 31 | }, nil 32 | } 33 | 34 | func (s StoragePool) MarshalToXML() (string, error) { 35 | return s.doc.Marshal() 36 | } 37 | 38 | func (s StoragePool) Type() string { 39 | return s.root.getAttribute(nameForLocal("type")) 40 | } 41 | 42 | func (s StoragePool) SetType(value string) { 43 | s.root.setAttribute(nameForLocal("type"), value) 44 | } 45 | 46 | func (s StoragePool) Target() StoragePoolTarget { 47 | node := s.root.ensureNode(nameForLocal("target")) 48 | return newStoragePoolTarget(node) 49 | } 50 | -------------------------------------------------------------------------------- /libvirtxml/storagePoolTarget.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StoragePoolTarget struct { 4 | node *Node 5 | } 6 | 7 | func newStoragePoolTarget(node *Node) StoragePoolTarget { 8 | return StoragePoolTarget{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s StoragePoolTarget) Path() string { 14 | return s.node.ensureNode(nameForLocal("path")).CharData 15 | } 16 | 17 | func (s StoragePoolTarget) SetPath(value string) { 18 | s.node.ensureNode(nameForLocal("path")).CharData = value 19 | } 20 | -------------------------------------------------------------------------------- /libvirtxml/storageVolume.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StorageVolume struct { 4 | doc *Document 5 | root *Node 6 | } 7 | 8 | func NewStorageVolume() StorageVolume { 9 | doc := &Document{} 10 | doc.Root = NewNode(nameForLocal("volume")) 11 | 12 | return StorageVolume{ 13 | doc: doc, 14 | root: doc.Root, 15 | } 16 | } 17 | 18 | func NewStorageVolumeForXML(xmlDoc string) (*StorageVolume, error) { 19 | doc := &Document{} 20 | if err := doc.Unmarshal(xmlDoc); err != nil { 21 | return nil, err 22 | } 23 | 24 | if doc.Root == nil { 25 | doc.Root = NewNode(nameForLocal("volume")) 26 | } 27 | 28 | return &StorageVolume{ 29 | doc: doc, 30 | root: doc.Root, 31 | }, nil 32 | } 33 | 34 | func (s StorageVolume) MarshalToXML() (string, error) { 35 | return s.doc.Marshal() 36 | } 37 | 38 | func (s StorageVolume) Type() string { 39 | return s.root.getAttribute(nameForLocal("type")) 40 | } 41 | 42 | func (s StorageVolume) SetType(value string) { 43 | s.root.setAttribute(nameForLocal("type"), value) 44 | } 45 | 46 | func (s StorageVolume) Name() string { 47 | return s.root.ensureNode(nameForLocal("name")).CharData 48 | } 49 | 50 | func (s StorageVolume) SetName(value string) { 51 | s.root.ensureNode(nameForLocal("name")).CharData = value 52 | } 53 | 54 | func (s StorageVolume) Key() string { 55 | return s.root.ensureNode(nameForLocal("key")).CharData 56 | } 57 | 58 | func (s StorageVolume) SetKey(value string) { 59 | s.root.ensureNode(nameForLocal("key")).CharData = value 60 | } 61 | 62 | func (s StorageVolume) Capacity() StorageVolumeSize { 63 | node := s.root.ensureNode(nameForLocal("capacity")) 64 | return newStorageVolumeSize(node) 65 | } 66 | 67 | func (s StorageVolume) Allocation() StorageVolumeSize { 68 | node := s.root.ensureNode(nameForLocal("allocation")) 69 | return newStorageVolumeSize(node) 70 | } 71 | 72 | func (s StorageVolume) Target() StorageVolumeTarget { 73 | node := s.root.ensureNode(nameForLocal("target")) 74 | return newStorageVolumeTarget(node) 75 | } 76 | 77 | func (s StorageVolume) BackingStore() StorageVolumeBackingStore { 78 | node := s.root.ensureNode(nameForLocal("backingStore")) 79 | return newStorageVolumeBackingStore(node) 80 | } 81 | -------------------------------------------------------------------------------- /libvirtxml/storageVolumeBackingStore.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StorageVolumeBackingStore struct { 4 | node *Node 5 | } 6 | 7 | func newStorageVolumeBackingStore(node *Node) StorageVolumeBackingStore { 8 | return StorageVolumeBackingStore{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s StorageVolumeBackingStore) Path() string { 14 | return s.node.ensureNode(nameForLocal("path")).CharData 15 | } 16 | 17 | func (s StorageVolumeBackingStore) SetPath(value string) { 18 | s.node.ensureNode(nameForLocal("path")).CharData = value 19 | } 20 | 21 | func (s StorageVolumeBackingStore) RemoveTimestamps() { 22 | s.node.removeNodes(nameForLocal("timestamps")) 23 | } 24 | 25 | func (s StorageVolumeBackingStore) Format() StorageVolumeTargetFormat { 26 | node := s.node.ensureNode(nameForLocal("format")) 27 | return newStorageVolumeTargetFormat(node) 28 | } 29 | 30 | func (s StorageVolumeBackingStore) Permissions() StorageVolumePermissions { 31 | node := s.node.ensureNode(nameForLocal("permissions")) 32 | return newStorageVolumePermissions(node) 33 | } 34 | -------------------------------------------------------------------------------- /libvirtxml/storageVolumePermissions.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/golang/glog" 7 | ) 8 | 9 | type StorageVolumePermissions struct { 10 | node *Node 11 | } 12 | 13 | func newStorageVolumePermissions(node *Node) StorageVolumePermissions { 14 | return StorageVolumePermissions{ 15 | node: node, 16 | } 17 | } 18 | 19 | func (s StorageVolumePermissions) Owner() uint64 { 20 | node := s.node.ensureNode(nameForLocal("owner")) 21 | 22 | str := node.CharData 23 | if str == "" { 24 | return 0 25 | } 26 | 27 | result, err := strconv.Atoi(str) 28 | if err != nil || result < 0 { 29 | result = 0 30 | glog.Warningf("libvirtxml: ignoring invalid storage volume owner '%s'", str) 31 | } 32 | 33 | return uint64(result) 34 | } 35 | 36 | func (s StorageVolumePermissions) SetOwner(value uint64) { 37 | node := s.node.ensureNode(nameForLocal("owner")) 38 | node.CharData = strconv.FormatUint(value, 10) 39 | } 40 | 41 | func (s StorageVolumePermissions) Group() uint64 { 42 | node := s.node.ensureNode(nameForLocal("group")) 43 | 44 | str := node.CharData 45 | if str == "" { 46 | return 0 47 | } 48 | 49 | result, err := strconv.Atoi(str) 50 | if err != nil || result < 0 { 51 | result = 0 52 | glog.Warningf("libvirtxml: ignoring invalid storage volume group '%s'", str) 53 | } 54 | 55 | return uint64(result) 56 | } 57 | 58 | func (s StorageVolumePermissions) SetGroup(value uint64) { 59 | node := s.node.ensureNode(nameForLocal("group")) 60 | node.CharData = strconv.FormatUint(value, 10) 61 | } 62 | 63 | func (s StorageVolumePermissions) Mode() string { 64 | return s.node.ensureNode(nameForLocal("mode")).CharData 65 | } 66 | 67 | func (s StorageVolumePermissions) SetMode(value string) { 68 | s.node.ensureNode(nameForLocal("mode")).CharData = value 69 | } 70 | -------------------------------------------------------------------------------- /libvirtxml/storageVolumeSize.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/golang/glog" 7 | ) 8 | 9 | type StorageVolumeSize struct { 10 | node *Node 11 | } 12 | 13 | func newStorageVolumeSize(node *Node) StorageVolumeSize { 14 | return StorageVolumeSize{ 15 | node: node, 16 | } 17 | } 18 | 19 | func (s StorageVolumeSize) Unit() string { 20 | return s.node.getAttribute(nameForLocal("unit")) 21 | } 22 | 23 | func (s StorageVolumeSize) SetUnit(value string) { 24 | s.node.setAttribute(nameForLocal("unit"), value) 25 | } 26 | 27 | func (s StorageVolumeSize) Value() uint64 { 28 | str := s.node.CharData 29 | if str == "" { 30 | return 0 31 | } 32 | 33 | result, err := strconv.Atoi(str) 34 | if err != nil || result < 0 { 35 | result = 0 36 | glog.Warningf("libvirtxml: ignoring invalid storage volume size '%s'", str) 37 | } 38 | 39 | return uint64(result) 40 | } 41 | 42 | func (s StorageVolumeSize) SetValue(value uint64) { 43 | s.node.CharData = strconv.FormatUint(value, 10) 44 | } 45 | -------------------------------------------------------------------------------- /libvirtxml/storageVolumeTarget.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StorageVolumeTarget struct { 4 | node *Node 5 | } 6 | 7 | func newStorageVolumeTarget(node *Node) StorageVolumeTarget { 8 | return StorageVolumeTarget{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s StorageVolumeTarget) Path() string { 14 | return s.node.ensureNode(nameForLocal("path")).CharData 15 | } 16 | 17 | func (s StorageVolumeTarget) SetPath(value string) { 18 | s.node.ensureNode(nameForLocal("path")).CharData = value 19 | } 20 | 21 | func (s StorageVolumeTarget) RemoveTimestamps() { 22 | s.node.removeNodes(nameForLocal("timestamps")) 23 | } 24 | 25 | func (s StorageVolumeTarget) Format() StorageVolumeTargetFormat { 26 | node := s.node.ensureNode(nameForLocal("format")) 27 | return newStorageVolumeTargetFormat(node) 28 | } 29 | 30 | func (s StorageVolumeTarget) Permissions() StorageVolumePermissions { 31 | node := s.node.ensureNode(nameForLocal("permissions")) 32 | return newStorageVolumePermissions(node) 33 | } 34 | -------------------------------------------------------------------------------- /libvirtxml/storageVolumeTargetFormat.go: -------------------------------------------------------------------------------- 1 | package libvirtxml 2 | 3 | type StorageVolumeTargetFormat struct { 4 | node *Node 5 | } 6 | 7 | func newStorageVolumeTargetFormat(node *Node) StorageVolumeTargetFormat { 8 | return StorageVolumeTargetFormat{ 9 | node: node, 10 | } 11 | } 12 | 13 | func (s StorageVolumeTargetFormat) Type() string { 14 | return s.node.getAttribute(nameForLocal("type")) 15 | } 16 | 17 | func (s StorageVolumeTargetFormat) SetType(value string) { 18 | s.node.setAttribute(nameForLocal("type"), value) 19 | } 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/bcrusu/kcm/cmd" 7 | "github.com/golang/glog" 8 | ) 9 | 10 | func main() { 11 | flag.Parse() 12 | 13 | if err := cmd.RootCmd.Execute(); err != nil { 14 | glog.V(2).Info(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /repository/cluster.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "github.com/bcrusu/kcm/util" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type Cluster struct { 13 | Name string `json:"name"` 14 | KubernetesVersion string `json:"kubernetesVersion"` 15 | CNIVersion string `json:"cniVersion"` 16 | CoreOSVersion string `json:"coreOSVersion"` 17 | CoreOSChannel string `json:"coreOSChannel"` 18 | Nodes map[string]Node `json:"nodes"` //map[NODE_NAME]NODE 19 | Network Network `json:"network"` 20 | StoragePool string `json:"storagePool"` 21 | BackingStorageVolume string `json:"backingStorageVolume"` 22 | CACertificate []byte `json:"caCertificate"` 23 | CAPrivateKey []byte `json:"caPrivateKey"` 24 | DNSDomain string `json:"dnsDomain"` 25 | ServerURL string `json:"ServerUrl"` 26 | } 27 | 28 | type Node struct { 29 | Name string `json:"name"` 30 | IsMaster bool `json:"isMaster"` 31 | Domain string `json:"domain"` 32 | MemoryMiB uint `json:"memory"` 33 | CPUs uint `json:"cpus"` 34 | VolumeCapacityGiB uint `json:"volumeCapacity"` 35 | StoragePool string `json:"storagePool"` 36 | BackingStorageVolume string `json:"backingStorageVolume"` 37 | StorageVolume string `json:"storageVolume"` 38 | DNSName string `json:"dnsName"` 39 | } 40 | 41 | type Network struct { 42 | Name string `json:"name"` 43 | IPv4CIDR string `json:"ipv4cidr"` 44 | } 45 | 46 | func loadCluster(clusterFile string) (*Cluster, error) { 47 | bytes, err := ioutil.ReadFile(clusterFile) 48 | if err != nil { 49 | return nil, errors.Wrapf(err, "repository: failed to read cluster '%s'", clusterFile) 50 | } 51 | 52 | cluster := &Cluster{} 53 | if err := json.Unmarshal(bytes, cluster); err != nil { 54 | return nil, errors.Wrapf(err, "repository: failed to unmarshall cluster '%s'", clusterFile) 55 | } 56 | 57 | if err := cluster.Validate(); err != nil { 58 | return nil, errors.Wrapf(err, "repository: validation failed for cluster '%s'", clusterFile) 59 | } 60 | 61 | return cluster, nil 62 | } 63 | 64 | func (c *Cluster) save(clusterFile string) error { 65 | if err := c.Validate(); err != nil { 66 | return errors.Wrapf(err, "repository: cluster '%s' validation failed", c.Name) 67 | } 68 | 69 | bytes, err := json.MarshalIndent(c, "", " ") 70 | if err != nil { 71 | return errors.Wrapf(err, "repository: failed to marshall cluster '%s'", c.Name) 72 | } 73 | 74 | if err := util.WriteFile(clusterFile, bytes); err != nil { 75 | return errors.Wrapf(err, "repository: failed to write cluster '%s'", clusterFile) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (c *Cluster) Validate() error { 82 | if c.Name == "" { 83 | return errors.New("missing cluster name") 84 | } 85 | 86 | if err := util.IsDNS1123Label(c.Name); err != nil { 87 | return err 88 | } 89 | 90 | if len(strings.TrimSpace(c.Name)) != len(c.Name) { 91 | return errors.New("invalid cluster name - cannot start/end with whitespaces") 92 | } 93 | 94 | if c.KubernetesVersion == "" { 95 | return errors.New("missing Kubernetes version") 96 | } 97 | 98 | if c.CNIVersion == "" { 99 | return errors.New("missing CNI version") 100 | } 101 | 102 | if c.CoreOSChannel == "" || c.CoreOSVersion == "" { 103 | return errors.New("invalid CoreOS version/channel") 104 | } 105 | 106 | if len(c.Nodes) < 1 { 107 | return errors.New("no node configured") 108 | } 109 | 110 | mastersCount := 0 111 | for _, node := range c.Nodes { 112 | if err := node.Validate(); err != nil { 113 | return err 114 | } 115 | 116 | if node.IsMaster { 117 | mastersCount++ 118 | } 119 | } 120 | 121 | if mastersCount == 0 { 122 | return errors.New("no master node configured") 123 | } 124 | 125 | if mastersCount != 1 { 126 | return errors.New("multiple master clusters are not supported atm") 127 | } 128 | 129 | if c.StoragePool == "" { 130 | return errors.New("missing storage pool name") 131 | } 132 | 133 | if c.BackingStorageVolume == "" { 134 | return errors.New("missing backing storage volume") 135 | } 136 | 137 | if c.ServerURL == "" { 138 | return errors.New("missing server URL") 139 | } 140 | 141 | if err := c.Network.validate(); err != nil { 142 | return err 143 | } 144 | 145 | if c.CACertificate == nil { 146 | return errors.New("missing CA certificate") 147 | } 148 | 149 | if c.CAPrivateKey == nil { 150 | return errors.New("missing CA private key") 151 | } 152 | 153 | if c.DNSDomain == "" { 154 | return errors.New("missing cluster DNS domain name") 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func (n *Node) Validate() error { 161 | if n == nil { 162 | return errors.Errorf("nil node") 163 | } 164 | 165 | if n.Name == "" { 166 | return errors.Errorf("missing node name") 167 | } 168 | 169 | if err := util.IsDNS1123Label(n.Name); err != nil { 170 | return err 171 | } 172 | 173 | if n.Domain == "" { 174 | return errors.Errorf("missing node domain") 175 | } 176 | 177 | if n.StorageVolume == "" { 178 | return errors.Errorf("missing node storage volume") 179 | } 180 | 181 | if n.StoragePool == "" { 182 | return errors.Errorf("missing node storage pool") 183 | } 184 | 185 | if n.BackingStorageVolume == "" { 186 | return errors.Errorf("missing backing storage volume") 187 | } 188 | 189 | if n.CPUs < 1 { 190 | return errors.Errorf("invalid CPUs value") 191 | } 192 | 193 | if n.MemoryMiB < 128 { 194 | return errors.Errorf("invalid memory value") 195 | } 196 | 197 | if n.VolumeCapacityGiB < 2 { 198 | return errors.Errorf("invalid volume capacity value") 199 | } 200 | 201 | if n.DNSName == "" { 202 | return errors.New("missing node DNS name") 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func (n *Network) validate() error { 209 | if n == nil { 210 | return errors.Errorf("nil network") 211 | } 212 | 213 | if n.Name == "" { 214 | return errors.Errorf("missing network name") 215 | } 216 | 217 | if n.IPv4CIDR == "" { 218 | return errors.Errorf("missing network CIDR") 219 | } 220 | 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /repository/clusterRepository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "github.com/bcrusu/kcm/util" 10 | "github.com/golang/glog" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const currentClusterFileName = "CURRENT" 15 | const clusterFileExtension = ".json" 16 | 17 | type ClusterRepository interface { 18 | Current() (*Cluster, error) 19 | SetCurrent(name string) error 20 | Load(name string) (*Cluster, error) 21 | LoadAll() ([]*Cluster, error) 22 | Save(cluster Cluster) error 23 | Remove(name string) error 24 | Exists(name string) (bool, error) 25 | } 26 | 27 | type clusterRepository struct { 28 | path string 29 | currentCluster *string 30 | } 31 | 32 | func New(path string) (ClusterRepository, error) { 33 | if err := util.CreateDirectoryPath(path); err != nil { 34 | return nil, errors.Wrapf(err, "repository: failed to initialize cluster repository '%s'", path) 35 | } 36 | 37 | result := &clusterRepository{ 38 | path: path, 39 | } 40 | 41 | if err := result.loadCurrentClusterName(); err != nil { 42 | return nil, err 43 | } 44 | 45 | return result, nil 46 | } 47 | 48 | func (r *clusterRepository) LoadAll() ([]*Cluster, error) { 49 | var result []*Cluster 50 | 51 | files, err := ioutil.ReadDir(r.path) 52 | if err != nil { 53 | return nil, errors.Wrapf(err, "repository: failed to read cluster repository dir '%s'", r.path) 54 | } 55 | 56 | for _, file := range files { 57 | if file.IsDir() { 58 | continue 59 | } 60 | 61 | fileName := file.Name() 62 | if !strings.HasSuffix(fileName, clusterFileExtension) { 63 | continue 64 | } 65 | 66 | cluster, err := loadCluster(path.Join(r.path, fileName)) 67 | if err != nil { 68 | glog.Warningf("repository: failed to load cluster from file '%s'", fileName) 69 | continue 70 | } 71 | 72 | result = append(result, cluster) 73 | } 74 | 75 | return result, nil 76 | } 77 | 78 | func (r *clusterRepository) Load(name string) (*Cluster, error) { 79 | filePath := r.clusterFile(name) 80 | 81 | exists, err := util.FileExists(filePath) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | if !exists { 87 | return nil, nil 88 | } 89 | 90 | return loadCluster(filePath) 91 | } 92 | 93 | func (r *clusterRepository) Current() (*Cluster, error) { 94 | if r.currentCluster == nil { 95 | return nil, nil 96 | } 97 | 98 | return r.Load(*r.currentCluster) 99 | } 100 | 101 | func (r *clusterRepository) SetCurrent(name string) error { 102 | if name == "" { 103 | return r.clearCurrentClusterName() 104 | } 105 | 106 | filePath := path.Join(r.path, currentClusterFileName) 107 | data := []byte(name) 108 | 109 | err := util.WriteFile(filePath, data) 110 | if err != nil { 111 | return errors.Wrapf(err, "repository: failed to set cluster '%s' as current cluster", name) 112 | } 113 | 114 | r.currentCluster = &name 115 | return nil 116 | } 117 | 118 | func (r *clusterRepository) Save(cluster Cluster) error { 119 | filePath := r.clusterFile(cluster.Name) 120 | if err := cluster.save(filePath); err != nil { 121 | return err 122 | } 123 | 124 | if r.currentCluster == nil { 125 | return r.SetCurrent(cluster.Name) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (r *clusterRepository) Remove(name string) error { 132 | if name == "" { 133 | return errors.New("repository: invalid cluster name") 134 | } 135 | 136 | filePath := r.clusterFile(name) 137 | err := os.Remove(filePath) 138 | if err != nil { 139 | return errors.Wrapf(err, "repository: failed to remove cluster '%s'", name) 140 | } 141 | 142 | if r.currentCluster != nil && name == *r.currentCluster { 143 | return r.clearCurrentClusterName() 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (r *clusterRepository) Exists(name string) (bool, error) { 150 | filePath := r.clusterFile(name) 151 | 152 | _, err := os.Stat(filePath) 153 | if err != nil { 154 | if os.IsNotExist(err) { 155 | return false, nil 156 | } 157 | 158 | return false, err 159 | } 160 | 161 | return true, nil 162 | } 163 | 164 | func (r *clusterRepository) loadCurrentClusterName() error { 165 | filePath := path.Join(r.path, currentClusterFileName) 166 | bytes, err := ioutil.ReadFile(filePath) 167 | if err != nil { 168 | if os.IsNotExist(err) { 169 | return nil 170 | } 171 | 172 | return errors.Wrap(err, "repository: failed to load current cluster") 173 | } 174 | 175 | name := strings.TrimSpace(string(bytes)) 176 | r.currentCluster = &name 177 | return nil 178 | } 179 | 180 | func (r *clusterRepository) clearCurrentClusterName() error { 181 | filePath := path.Join(r.path, currentClusterFileName) 182 | err := os.Remove(filePath) 183 | if err != nil { 184 | if os.IsNotExist(err) { 185 | return nil 186 | } 187 | 188 | return errors.Wrap(err, "repository: failed to clear current cluster name") 189 | } 190 | 191 | r.currentCluster = nil 192 | return nil 193 | } 194 | 195 | func (r *clusterRepository) clusterFile(clusterName string) string { 196 | fileName := clusterName + clusterFileExtension 197 | return path.Join(r.path, fileName) 198 | } 199 | -------------------------------------------------------------------------------- /util/archive.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "path" 7 | 8 | "os" 9 | 10 | "github.com/golang/glog" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func ExtractTar(in io.Reader, outDir string) error { 15 | tarReader := tar.NewReader(in) 16 | 17 | for { 18 | hdr, err := tarReader.Next() 19 | if err == io.EOF { 20 | break 21 | } 22 | 23 | if err != nil { 24 | return err 25 | } 26 | 27 | filePath := path.Join(outDir, hdr.Name) 28 | 29 | switch hdr.Typeflag { 30 | case tar.TypeReg, tar.TypeRegA: 31 | fileMode := hdr.FileInfo().Mode() 32 | fileMode |= os.FileMode(0044) 33 | if fileMode&0100 != 0 { 34 | fileMode |= os.FileMode(001) 35 | } 36 | 37 | file, err := CreateFile(filePath, fileMode) 38 | if err != nil { 39 | return errors.Wrapf(err, "extract tar: failed to create file '%s'", filePath) 40 | } 41 | 42 | if _, err := io.Copy(file, tarReader); err != nil { 43 | return errors.Wrapf(err, "extract tar: failed to write file contents '%s'", filePath) 44 | } 45 | 46 | if err := file.Close(); err != nil { 47 | return errors.Wrapf(err, "extract tar: failed to close file '%s'", filePath) 48 | } 49 | case tar.TypeDir: 50 | if err := CreateDirectoryPath(filePath); err != nil { 51 | return errors.Wrapf(err, "extract tar: failed to create directory '%s'", filePath) 52 | } 53 | default: 54 | glog.Warningf("extract tar: skipped special type header '%s'", hdr.Name) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /util/certs.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "math/big" 10 | "net" 11 | "time" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func CreateCACertificate(commonName string) (cert []byte, key []byte, err error) { 17 | template, err := newCertificateTemplate(commonName) 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | 22 | template.IsCA = true 23 | template.KeyUsage |= x509.KeyUsageCertSign | x509.KeyUsageCRLSign 24 | 25 | return newCertificate(template, nil, nil, 2048) 26 | } 27 | 28 | func CreateServerCertificate(commonName string, signer *x509.Certificate, signerKey *rsa.PrivateKey, hosts ...string) (cert []byte, key []byte, err error) { 29 | template, err := newCertificateTemplate(commonName, hosts...) 30 | if err != nil { 31 | return nil, nil, err 32 | } 33 | 34 | template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} 35 | 36 | return newCertificate(template, signer, signerKey, 2048) 37 | } 38 | 39 | func CreateClientCertificate(commonName string, signer *x509.Certificate, signerKey *rsa.PrivateKey, hosts ...string) (cert []byte, key []byte, err error) { 40 | template, err := newCertificateTemplate(commonName, hosts...) 41 | if err != nil { 42 | return nil, nil, err 43 | } 44 | 45 | template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} 46 | 47 | return newCertificate(template, signer, signerKey, 2048) 48 | } 49 | 50 | func ParseCertificate(data []byte) (*x509.Certificate, error) { 51 | block, _ := pem.Decode(data) 52 | if block == nil { 53 | return nil, errors.New("certs: input does not contain a PEM block") 54 | } 55 | 56 | if block.Type != "CERTIFICATE" { 57 | return nil, errors.New("certs: input does not contain a x509 certificate") 58 | } 59 | 60 | return x509.ParseCertificate(block.Bytes) 61 | } 62 | 63 | func ParsePrivateKey(data []byte) (*rsa.PrivateKey, error) { 64 | block, _ := pem.Decode(data) 65 | if block == nil { 66 | return nil, errors.New("certs: input does not contain a PEM block") 67 | } 68 | 69 | if block.Type != "RSA PRIVATE KEY" { 70 | return nil, errors.New("certs: input does not contain a RSA private key") 71 | } 72 | 73 | return x509.ParsePKCS1PrivateKey(block.Bytes) 74 | } 75 | 76 | func newCertificate(template, signer *x509.Certificate, signerKey *rsa.PrivateKey, rsaBits int) (cert []byte, key []byte, err error) { 77 | privateKey, err := rsa.GenerateKey(rand.Reader, rsaBits) 78 | if err != nil { 79 | return nil, nil, errors.Wrapf(err, "certs: failed to generate RSA private key") 80 | } 81 | 82 | if signer == nil { 83 | signer = template 84 | signerKey = privateKey 85 | } 86 | 87 | derBytes, err := x509.CreateCertificate(rand.Reader, template, signer, &privateKey.PublicKey, signerKey) 88 | if err != nil { 89 | return nil, nil, errors.Wrapf(err, "certs: failed to create certificate") 90 | } 91 | 92 | cert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 93 | key = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) 94 | 95 | return cert, key, nil 96 | } 97 | 98 | func newCertificateTemplate(commonName string, hosts ...string) (*x509.Certificate, error) { 99 | notBefore := time.Now() 100 | notAfter := notBefore.Add(365 * 24 * time.Hour) 101 | 102 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 103 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 104 | if err != nil { 105 | return nil, errors.Wrapf(err, "certs: failed to generate serial number") 106 | } 107 | 108 | template := &x509.Certificate{ 109 | SerialNumber: serialNumber, 110 | Subject: pkix.Name{ 111 | CommonName: commonName, 112 | Organization: []string{"kcm"}, 113 | }, 114 | NotBefore: notBefore, 115 | NotAfter: notAfter, 116 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 117 | BasicConstraintsValid: true, 118 | } 119 | 120 | for _, n := range hosts { 121 | if ip := net.ParseIP(n); ip != nil { 122 | template.IPAddresses = append(template.IPAddresses, ip) 123 | } else { 124 | template.DNSNames = append(template.DNSNames, n) 125 | } 126 | } 127 | 128 | return template, nil 129 | } 130 | -------------------------------------------------------------------------------- /util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func CreateFile(fileName string, perm os.FileMode) (*os.File, error) { 11 | file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 12 | if err != nil { 13 | return nil, errors.Wrapf(err, "failed to create file '%s'", fileName) 14 | } 15 | 16 | return file, nil 17 | } 18 | 19 | func DirectoryExists(path string) (bool, error) { 20 | stat, err := os.Stat(path) 21 | if err != nil { 22 | if os.IsNotExist(err) { 23 | return false, nil 24 | } 25 | return false, err 26 | } 27 | 28 | if !stat.IsDir() { 29 | return false, errors.Errorf("path is not a directory '%s'", path) 30 | } 31 | 32 | return true, nil 33 | } 34 | 35 | func FileExists(path string) (bool, error) { 36 | stat, err := os.Stat(path) 37 | if err != nil { 38 | if os.IsNotExist(err) { 39 | return false, nil 40 | } 41 | return false, err 42 | } 43 | 44 | if stat.IsDir() { 45 | return false, errors.Errorf("path is not a file '%s'", path) 46 | } 47 | 48 | return true, nil 49 | } 50 | 51 | func CreateDirectoryPath(path string) error { 52 | if err := os.MkdirAll(path, 0755); err != nil { 53 | return errors.Wrapf(err, "failed to create directory '%s'", path) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func RemoveDirectory(path string) error { 60 | if err := os.RemoveAll(path); err != nil { 61 | return errors.Wrapf(err, "failed to remove directory '%s'", path) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func WriteFile(path string, data []byte) error { 68 | if err := ioutil.WriteFile(path, data, 0644); err != nil { 69 | return errors.Wrapf(err, "failed to write to file '%s'", path) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func WriteExecutableFile(path string, data []byte) error { 76 | if err := ioutil.WriteFile(path, data, 0655); err != nil { 77 | return errors.Wrapf(err, "failed to write to file '%s'", path) 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func DownloadHTTP(url string) (io.ReadCloser, error) { 12 | client := &http.Client{ 13 | Timeout: time.Hour, 14 | } 15 | 16 | response, err := client.Get(url) 17 | if err != nil { 18 | return nil, errors.Wrapf(err, "http: failed to download '%s'", url) 19 | } 20 | 21 | if response.StatusCode != 200 { 22 | return nil, errors.Errorf("http: failed to download '%s'. Error code: %d", url, response.StatusCode) 23 | } 24 | 25 | return response.Body, nil 26 | } 27 | -------------------------------------------------------------------------------- /util/io.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | func CloseNoError(c io.Closer) { 8 | if err := c.Close(); err != nil { 9 | panic(err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /util/net.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type NetworkInfo struct { 11 | Family string // ipv4 or ipv6 12 | BridgeIP net.IP 13 | DHCPRangeStart net.IP 14 | DHCPRangeEnd net.IP 15 | Net *net.IPNet 16 | } 17 | 18 | func ParseNetworkCIDR(cidr string) (*NetworkInfo, error) { 19 | _, ipnet, err := net.ParseCIDR(cidr) 20 | if err != nil { 21 | return nil, errors.Wrapf(err, "net: failed to parse CIDR '%s'", cidr) 22 | } 23 | 24 | var family string 25 | switch len(ipnet.IP) { 26 | case net.IPv4len: 27 | family = "ipv4" 28 | case net.IPv6len: 29 | family = "ipv6" 30 | default: 31 | return nil, errors.Wrapf(err, "net: failed to parse CIDR '%s'", cidr) 32 | } 33 | 34 | dhcpStart, dhcpEnd := getDHCPRange(ipnet) 35 | 36 | return &NetworkInfo{ 37 | Family: family, 38 | BridgeIP: getBridgeIP(ipnet), 39 | DHCPRangeStart: dhcpStart, 40 | DHCPRangeEnd: dhcpEnd, 41 | Net: ipnet, 42 | }, nil 43 | } 44 | 45 | func getBridgeIP(net *net.IPNet) net.IP { 46 | result := make([]byte, len(net.IP)) 47 | copy(result, net.IP) 48 | 49 | result[len(result)-1]++ 50 | return result 51 | } 52 | 53 | func getDHCPRange(ipnet *net.IPNet) (net.IP, net.IP) { 54 | ipLen := len(ipnet.IP) 55 | 56 | start := make([]byte, ipLen) 57 | { 58 | copy(start, ipnet.IP) 59 | start[ipLen-1] += 2 // first IP is assigned to the bridge 60 | } 61 | 62 | end := make([]byte, ipLen) 63 | { 64 | copy(end, ipnet.IP) 65 | 66 | for i, b := range ipnet.Mask { 67 | end[i] += ^b 68 | } 69 | 70 | if ipLen == net.IPv4len { 71 | end[ipLen-1]-- // exclude broadcast address 72 | } 73 | } 74 | 75 | return start, end 76 | } 77 | 78 | const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" 79 | const dns1123LabelErrMsg string = "a DNS-1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character" 80 | const DNS1123LabelMaxLength int = 63 81 | 82 | var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") 83 | 84 | // stolen from here: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go 85 | // IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). 86 | func IsDNS1123Label(value string) error { 87 | if len(value) > DNS1123LabelMaxLength { 88 | return errors.Errorf("net: invalid DNS label '%s' - max length of 63 chars exceeded", value) 89 | } 90 | 91 | if !dns1123LabelRegexp.MatchString(value) { 92 | return errors.Errorf("net: invalid DNS label '%s' - "+dns1123LabelErrMsg, value) 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /util/os.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | func ExecCommandAndWait(name string, arg ...string) error { 9 | cmd := exec.Command(name, arg...) 10 | cmd.Stdout = os.Stdout 11 | cmd.Stdin = os.Stdin 12 | cmd.Stderr = os.Stderr 13 | 14 | if err := cmd.Run(); err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /util/tabwriter.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "text/tabwriter" 7 | ) 8 | 9 | const ( 10 | tabwriterMinWidth = 10 11 | tabwriterWidth = 4 12 | tabwriterPadding = 3 13 | tabwriterPadChar = ' ' 14 | tabwriterFlags = 0 15 | ) 16 | 17 | type TabWriter struct { 18 | writer *tabwriter.Writer 19 | } 20 | 21 | func NewTabWriter(output io.Writer) *TabWriter { 22 | writer := tabwriter.NewWriter(output, tabwriterMinWidth, tabwriterWidth, tabwriterPadding, tabwriterPadChar, tabwriterFlags) 23 | return &TabWriter{writer} 24 | } 25 | 26 | func (w *TabWriter) Print(str string) { 27 | fmt.Fprint(w.writer, str) 28 | } 29 | 30 | func (w *TabWriter) Nl() { 31 | w.Print("\n") 32 | } 33 | 34 | func (w *TabWriter) Tab() { 35 | w.Print("\t") 36 | } 37 | 38 | func (w *TabWriter) Flush() { 39 | w.writer.Flush() 40 | } 41 | -------------------------------------------------------------------------------- /util/template.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | ) 7 | 8 | func GenerateTextTemplate(templateStr string, params interface{}) []byte { 9 | t := template.New("t") 10 | 11 | if _, err := t.Parse(templateStr); err != nil { 12 | panic(err) 13 | } 14 | 15 | buffer := &bytes.Buffer{} 16 | if err := t.ExecuteTemplate(buffer, "t", params); err != nil { 17 | panic(err) 18 | } 19 | 20 | return buffer.Bytes() 21 | } 22 | -------------------------------------------------------------------------------- /util/user.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log" 5 | "os/user" 6 | "path" 7 | ) 8 | 9 | func GetUserHomeDir() string { 10 | usr, err := user.Current() 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | return usr.HomeDir 15 | } 16 | 17 | func GetUserDefaultSSHPublicKeyPath() string { 18 | home := GetUserHomeDir() 19 | return path.Join(home, ".ssh", "id_rsa.pub") 20 | } 21 | --------------------------------------------------------------------------------