blob: 6bff633760ad51d410bb1ea9eede07ba46c5ed73 [file] [log] [blame]
Mohammed Naserd4617bb2023-12-07 19:35:33 -05001// Copyright (c) 2023 VEXXHOST, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations
13// under the License.
14
15package main
16
17import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "net/url"
23 "os"
24 "path/filepath"
25 "strings"
26
27 "github.com/vexxhost/atmosphere/internal/portforwardutil"
28
29 "github.com/erikgeiser/promptkit/confirmation"
30 "github.com/nsf/jsondiff"
31 log "github.com/sirupsen/logrus"
32 "github.com/spf13/cobra"
33 "gopkg.in/ini.v1"
34 "gorm.io/driver/mysql"
35 "gorm.io/gorm"
36 v1 "k8s.io/api/core/v1"
37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38 "k8s.io/client-go/kubernetes"
39 "k8s.io/client-go/rest"
40 "k8s.io/client-go/tools/clientcmd"
41 "k8s.io/client-go/tools/portforward"
42 "k8s.io/client-go/util/homedir"
43)
44
45var (
46 kubeconfig string
47
48 portForward *portforward.PortForwarder
49 db *gorm.DB
50 instanceExtra InstanceExtra
51 key string
52
53 flavor *InstanceExtraFlavor
54
55 rootCmd = &cobra.Command{
56 Use: "extraspecfix",
57 Short: "Utility to fix extra_specs in the database",
58 PersistentPreRun: func(cmd *cobra.Command, args []string) {
59 instanceUUID := args[0]
60 key = args[1]
61
62 config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
63 if err != nil {
64 log.Fatal(err)
65 }
66
67 databaseConnection, err := GetDatabaseConnection(config)
68 if err != nil {
69 log.Fatal(err)
70 }
71
72 portForward, err = databaseConnection.PortForwarder()
73 if err != nil {
74 log.Fatal(err)
75 }
76
77 go func() {
78 err := portForward.ForwardPorts()
79 if err != nil {
80 log.Fatal(err)
81 }
82 }()
83
84 <-portForward.Ready
85
86 dsn := fmt.Sprintf(
87 "%s:%s@tcp(%s:%d)/%s",
88 databaseConnection.Username,
89 databaseConnection.Password,
90 "localhost",
91 3306,
92 databaseConnection.Database,
93 )
94 // TODO: write a custom dialer?
95 db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
96 if err != nil {
97 log.Fatal(err)
98 }
99
100 tx := db.Where(&InstanceExtra{InstanceUUID: instanceUUID}).First(&instanceExtra)
101 if tx.Error != nil {
102 log.Fatal(err)
103 }
104
105 err = StrictUnmarshal([]byte(instanceExtra.Flavor), &flavor)
106 if err != nil {
107 log.Fatal(err)
108 }
109 },
110 PersistentPostRun: func(cmd *cobra.Command, args []string) {
111 serializedFlavor, err := json.Marshal(flavor)
112 if err != nil {
113 log.Fatal(err)
114 }
115
116 diffOpts := jsondiff.DefaultConsoleOptions()
117 diff, output := jsondiff.Compare(
118 []byte(instanceExtra.Flavor),
119 []byte(serializedFlavor),
120 &diffOpts,
121 )
122
123 if diff == jsondiff.FullMatch {
124 log.Info("no changes")
125 os.Exit(0)
126 }
127
128 fmt.Println(output)
129
130 input := confirmation.New("Are you ready?", confirmation.Undecided)
131 ready, err := input.RunPrompt()
132 if err != nil || !ready {
133 log.Info("operation cancelled")
134 os.Exit(0)
135 }
136
137 db.Model(&instanceExtra).Update("flavor", string(serializedFlavor))
138 portForward.Close()
139 },
140 }
141 setCmd = &cobra.Command{
142 Use: "set [instance uuid] [key] [value]",
143 Short: "Set an extra spec on an instance",
144 Args: cobra.MatchAll(cobra.ExactArgs(3), cobra.OnlyValidArgs),
145 Run: func(cmd *cobra.Command, args []string) {
146 value := args[2]
147 flavor.Current.Data.ExtraSpecs[key] = value
148 },
149 }
150)
151
152func init() {
153 homedir := homedir.HomeDir()
154 rootCmd.PersistentFlags().StringVar(
155 &kubeconfig,
156 "kubeconfig",
157 filepath.Join(homedir, ".kube", "config"),
158 "absolute path to the kubeconfig file",
159 )
160
161 rootCmd.AddCommand(setCmd)
162}
163
164type InstanceExtra struct {
165 ID uint `gorm:"primaryKey"`
166 InstanceUUID string `gorm:"column:instance_uuid"`
167 Flavor string `gorm:"column:flavor"`
168}
169
170type InstanceExtraFlavor struct {
171 Old *InstanceExtraFlavorObject `json:"old"`
172 Current *InstanceExtraFlavorObject `json:"cur"`
173 New *InstanceExtraFlavorObject `json:"new"`
174}
175
176type InstanceExtraFlavorObject struct {
177 Name string `json:"nova_object.name"`
178 Namespace string `json:"nova_object.namespace"`
179 Version string `json:"nova_object.version"`
180 Changes []string `json:"nova_object.changes"`
181 Data InstanceExtraFlavorObjectData `json:"nova_object.data"`
182}
183
184type InstanceExtraFlavorObjectData struct {
185 ID int64 `json:"id"`
186 FlavorID string `json:"flavorid"`
187 Name string `json:"name"`
188 Description *string `json:"description"`
189 VCPUs int64 `json:"vcpus"`
190 VCPUWeight int64 `json:"vcpu_weight"`
191 MemoryMB int64 `json:"memory_mb"`
192 RootGB int64 `json:"root_gb"`
193 EphemeralGB int64 `json:"ephemeral_gb"`
194 Swap int64 `json:"swap"`
195 RxTxFactor json.Number `json:"rxtx_factor"`
196 IsPublic bool `json:"is_public"`
197 Disabled bool `json:"disabled"`
198 ExtraSpecs map[string]string `json:"extra_specs"`
199 CreatedAt string `json:"created_at"`
200 UpdatedAt *string `json:"updated_at"`
201 DeletedAt *string `json:"deleted_at"`
202 Deleted bool `json:"deleted"`
203}
204
205func (InstanceExtra) TableName() string {
206 return "instance_extra"
207}
208
209func main() {
210 if err := rootCmd.Execute(); err != nil {
211 fmt.Fprintln(os.Stderr, err)
212 os.Exit(1)
213 }
214}
215
216func StrictUnmarshal(data []byte, v interface{}) error {
217 dec := json.NewDecoder(bytes.NewReader(data))
218 dec.DisallowUnknownFields()
219 return dec.Decode(v)
220}
221
222type DatabaseConnection struct {
223 config *rest.Config
224 clientset *kubernetes.Clientset
225 Username string
226 Password string
227 Service *v1.Service
228 Database string
229}
230
231func (db *DatabaseConnection) PortForwarder() (*portforward.PortForwarder, error) {
232 return portforwardutil.NewForService(db.config, db.Service, 3306)
233}
234
235func GetDatabaseConnection(config *rest.Config) (*DatabaseConnection, error) {
236 clientset, err := kubernetes.NewForConfig(config)
237 if err != nil {
238 return nil, err
239 }
240
241 secret, err := clientset.CoreV1().Secrets("openstack").Get(
242 context.TODO(),
243 "nova-etc",
244 metav1.GetOptions{},
245 )
246 if err != nil {
247 return nil, err
248 }
249
250 cfg, err := ini.Load(secret.Data["nova.conf"])
251 if err != nil {
252 return nil, err
253 }
254
255 connection := cfg.Section("database").Key("connection").String()
256
257 parsedConnection, err := url.Parse(connection)
258 if err != nil {
259 return nil, err
260 }
261
262 username := parsedConnection.User.Username()
263 password, _ := parsedConnection.User.Password()
264 database := strings.TrimLeft(parsedConnection.Path, "/")
265
266 hostname := parsedConnection.Hostname()
267 parts := strings.SplitN(hostname, ".", 3)
268 service := &v1.Service{
269 ObjectMeta: metav1.ObjectMeta{
270 Name: parts[0],
271 Namespace: parts[1],
272 },
273 }
274
275 return &DatabaseConnection{
276 config: config,
277 clientset: clientset,
278 Username: username,
279 Password: password,
280 Service: service,
281 Database: database,
282 }, nil
283}