github.com/coreos/mantle@v0.13.0/platform/cluster.go (about) 1 // Copyright 2015 CoreOS, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain 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, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package platform 16 17 import ( 18 "bytes" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 "sync" 23 "time" 24 25 "github.com/pborman/uuid" 26 "golang.org/x/crypto/ssh" 27 "golang.org/x/crypto/ssh/agent" 28 29 "github.com/coreos/mantle/platform/conf" 30 "github.com/coreos/mantle/util" 31 ) 32 33 type BaseCluster struct { 34 machlock sync.Mutex 35 machmap map[string]Machine 36 consolemap map[string]string 37 38 bf *BaseFlight 39 name string 40 rconf *RuntimeConfig 41 } 42 43 func NewBaseCluster(bf *BaseFlight, rconf *RuntimeConfig) (*BaseCluster, error) { 44 bc := &BaseCluster{ 45 bf: bf, 46 machmap: make(map[string]Machine), 47 consolemap: make(map[string]string), 48 name: fmt.Sprintf("%s-%s", bf.baseopts.BaseName, uuid.New()), 49 rconf: rconf, 50 } 51 52 return bc, nil 53 } 54 55 func (bc *BaseCluster) SSHClient(ip string) (*ssh.Client, error) { 56 sshClient, err := bc.bf.agent.NewClient(ip) 57 if err != nil { 58 return nil, err 59 } 60 61 return sshClient, nil 62 } 63 64 func (bc *BaseCluster) UserSSHClient(ip, user string) (*ssh.Client, error) { 65 sshClient, err := bc.bf.agent.NewUserClient(ip, user) 66 if err != nil { 67 return nil, err 68 } 69 70 return sshClient, nil 71 } 72 73 func (bc *BaseCluster) PasswordSSHClient(ip string, user string, password string) (*ssh.Client, error) { 74 sshClient, err := bc.bf.agent.NewPasswordClient(ip, user, password) 75 if err != nil { 76 return nil, err 77 } 78 79 return sshClient, nil 80 } 81 82 // SSH executes the given command, cmd, on the given Machine, m. It returns the 83 // stdout and stderr of the command and an error. 84 // Leading and trailing whitespace is trimmed from each. 85 func (bc *BaseCluster) SSH(m Machine, cmd string) ([]byte, []byte, error) { 86 var stdout bytes.Buffer 87 var stderr bytes.Buffer 88 client, err := bc.SSHClient(m.IP()) 89 if err != nil { 90 return nil, nil, err 91 } 92 defer client.Close() 93 94 session, err := client.NewSession() 95 if err != nil { 96 return nil, nil, err 97 } 98 defer session.Close() 99 100 session.Stdout = &stdout 101 session.Stderr = &stderr 102 err = session.Run(cmd) 103 outBytes := bytes.TrimSpace(stdout.Bytes()) 104 errBytes := bytes.TrimSpace(stderr.Bytes()) 105 return outBytes, errBytes, err 106 } 107 108 func (bc *BaseCluster) Machines() []Machine { 109 bc.machlock.Lock() 110 defer bc.machlock.Unlock() 111 machs := make([]Machine, 0, len(bc.machmap)) 112 for _, m := range bc.machmap { 113 machs = append(machs, m) 114 } 115 return machs 116 } 117 118 func (bc *BaseCluster) AddMach(m Machine) { 119 bc.machlock.Lock() 120 defer bc.machlock.Unlock() 121 bc.machmap[m.ID()] = m 122 } 123 124 func (bc *BaseCluster) DelMach(m Machine) { 125 bc.machlock.Lock() 126 defer bc.machlock.Unlock() 127 delete(bc.machmap, m.ID()) 128 bc.consolemap[m.ID()] = m.ConsoleOutput() 129 } 130 131 func (bc *BaseCluster) Keys() ([]*agent.Key, error) { 132 return bc.bf.Keys() 133 } 134 135 func (bc *BaseCluster) RenderUserData(userdata *conf.UserData, ignitionVars map[string]string) (*conf.Conf, error) { 136 if userdata == nil { 137 switch bc.IgnitionVersion() { 138 case "v2": 139 userdata = conf.Ignition(`{"ignition": {"version": "2.0.0"}}`) 140 case "v3": 141 userdata = conf.Ignition(`{"ignition": {"version": "3.0.0"}}`) 142 default: 143 return nil, fmt.Errorf("unknown ignition version") 144 } 145 } 146 147 // hacky solution for unified ignition metadata variables 148 if userdata.IsIgnitionCompatible() { 149 for k, v := range ignitionVars { 150 userdata = userdata.Subst(k, v) 151 } 152 } 153 154 conf, err := userdata.Render(bc.bf.ctPlatform) 155 if err != nil { 156 return nil, err 157 } 158 159 for _, dropin := range bc.bf.baseopts.SystemdDropins { 160 conf.AddSystemdUnitDropin(dropin.Unit, dropin.Name, dropin.Contents) 161 } 162 163 if !bc.rconf.NoSSHKeyInUserData { 164 keys, err := bc.bf.Keys() 165 if err != nil { 166 return nil, err 167 } 168 169 conf.CopyKeys(keys) 170 } 171 172 // disable Zincati & Pinger by default 173 if bc.Distribution() == "fcos" { 174 conf.AddFile("/etc/fedora-coreos-pinger/config.d/90-disable-reporting.toml", "root", `[reporting] 175 enabled = false`, 0644) 176 conf.AddFile("/etc/zincati/config.d/90-disable-auto-updates.toml", "root", `[updates] 177 enabled = false`, 0644) 178 } 179 180 if bc.bf.baseopts.OSContainer != "" { 181 if bc.Distribution() != "rhcos" { 182 return nil, fmt.Errorf("oscontainer is only supported on the rhcos distribution") 183 } 184 conf.AddSystemdUnitDropin("pivot.service", "00-before-sshd.conf", `[Unit] 185 Before=sshd.service`) 186 conf.AddSystemdUnit("pivot.service", "", true) 187 conf.AddSystemdUnit("pivot-write-reboot-needed.service", `[Unit] 188 Description=Touch /run/pivot/reboot-needed 189 ConditionFirstBoot=true 190 191 [Service] 192 Type=oneshot 193 ExecStart=/usr/bin/mkdir -p /run/pivot 194 ExecStart=/usr/bin/touch /run/pivot/reboot-needed 195 196 [Install] 197 WantedBy=multi-user.target 198 `, true) 199 conf.AddFile("/etc/pivot/image-pullspec", "root", bc.bf.baseopts.OSContainer, 0644) 200 } 201 202 if conf.IsIgnition() { 203 if !conf.ValidConfig() { 204 return nil, fmt.Errorf("invalid ignition config") 205 } 206 } 207 208 return conf, nil 209 } 210 211 // Destroy destroys each machine in the cluster. 212 func (bc *BaseCluster) Destroy() { 213 for _, m := range bc.Machines() { 214 m.Destroy() 215 } 216 } 217 218 // XXX(mischief): i don't really think this belongs here, but it completes the 219 // interface we've established. 220 func (bc *BaseCluster) GetDiscoveryURL(size int) (string, error) { 221 var result string 222 err := util.Retry(3, 5*time.Second, func() error { 223 resp, err := http.Get(fmt.Sprintf("https://discovery.etcd.io/new?size=%d", size)) 224 if err != nil { 225 return err 226 } 227 defer resp.Body.Close() 228 if resp.StatusCode != 200 { 229 return fmt.Errorf("Discovery service returned %q", resp.Status) 230 } 231 232 body, err := ioutil.ReadAll(resp.Body) 233 if err != nil { 234 return err 235 } 236 result = string(body) 237 return nil 238 }) 239 return result, err 240 } 241 242 func (bc *BaseCluster) Distribution() string { 243 return bc.bf.baseopts.Distribution 244 } 245 246 func (bc *BaseCluster) IgnitionVersion() string { 247 return bc.bf.baseopts.IgnitionVersion 248 } 249 250 func (bc *BaseCluster) Platform() Name { 251 return bc.bf.Platform() 252 } 253 254 func (bc *BaseCluster) Name() string { 255 return bc.name 256 } 257 258 func (bc *BaseCluster) RuntimeConf() RuntimeConfig { 259 return *bc.rconf 260 } 261 262 func (bc *BaseCluster) ConsoleOutput() map[string]string { 263 ret := map[string]string{} 264 bc.machlock.Lock() 265 defer bc.machlock.Unlock() 266 for k, v := range bc.consolemap { 267 ret[k] = v 268 } 269 return ret 270 } 271 272 func (bc *BaseCluster) JournalOutput() map[string]string { 273 ret := map[string]string{} 274 bc.machlock.Lock() 275 defer bc.machlock.Unlock() 276 for k, v := range bc.machmap { 277 ret[k] = v.JournalOutput() 278 } 279 return ret 280 }