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  }