engine.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. package control
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net"
  6. "os"
  7. "path"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. "github.com/rancher/os/cmd/control/service"
  12. "github.com/rancher/os/cmd/control/service/app"
  13. "github.com/rancher/os/config"
  14. "github.com/rancher/os/pkg/compose"
  15. "github.com/rancher/os/pkg/docker"
  16. "github.com/rancher/os/pkg/log"
  17. "github.com/rancher/os/pkg/util"
  18. "github.com/rancher/os/pkg/util/network"
  19. yaml "github.com/cloudfoundry-incubator/candiedyaml"
  20. "github.com/codegangsta/cli"
  21. "github.com/docker/docker/reference"
  22. "github.com/docker/engine-api/types"
  23. "github.com/docker/engine-api/types/filters"
  24. composeConfig "github.com/docker/libcompose/config"
  25. "github.com/docker/libcompose/project/options"
  26. composeYaml "github.com/docker/libcompose/yaml"
  27. "github.com/pkg/errors"
  28. "golang.org/x/net/context"
  29. )
  30. func engineSubcommands() []cli.Command {
  31. return []cli.Command{
  32. {
  33. Name: "switch",
  34. Usage: "switch user Docker engine without a reboot",
  35. Action: engineSwitch,
  36. Flags: []cli.Flag{
  37. cli.BoolFlag{
  38. Name: "force, f",
  39. Usage: "do not prompt for input",
  40. },
  41. cli.BoolFlag{
  42. Name: "no-pull",
  43. Usage: "don't pull console image",
  44. },
  45. },
  46. },
  47. {
  48. Name: "create",
  49. Usage: "create Dind engine without a reboot",
  50. Description: "must switch user docker to 17.12.1 or earlier if using Dind",
  51. ArgsUsage: "<name>",
  52. Before: preFlightValidate,
  53. Action: engineCreate,
  54. Flags: []cli.Flag{
  55. cli.StringFlag{
  56. Name: "version, v",
  57. Value: config.DefaultDind,
  58. Usage: fmt.Sprintf("set the version for the engine, %s are available", config.SupportedDinds),
  59. },
  60. cli.StringFlag{
  61. Name: "network",
  62. Usage: "set the network for the engine",
  63. },
  64. cli.StringFlag{
  65. Name: "fixed-ip",
  66. Usage: "set the fixed ip for the engine",
  67. },
  68. cli.StringFlag{
  69. Name: "ssh-port",
  70. Usage: "set the ssh port for the engine",
  71. },
  72. cli.StringFlag{
  73. Name: "authorized-keys",
  74. Usage: "set the ssh authorized_keys absolute path for the engine",
  75. },
  76. },
  77. },
  78. {
  79. Name: "rm",
  80. Usage: "remove Dind engine without a reboot",
  81. ArgsUsage: "<name>",
  82. Before: func(c *cli.Context) error {
  83. if len(c.Args()) != 1 {
  84. return errors.New("Must specify exactly one Docker engine to remove")
  85. }
  86. return nil
  87. },
  88. Action: dindEngineRemove,
  89. Flags: []cli.Flag{
  90. cli.IntFlag{
  91. Name: "timeout,t",
  92. Usage: "specify a shutdown timeout in seconds",
  93. Value: 10,
  94. },
  95. cli.BoolFlag{
  96. Name: "force, f",
  97. Usage: "do not prompt for input",
  98. },
  99. },
  100. },
  101. {
  102. Name: "enable",
  103. Usage: "set user Docker engine to be switched on next reboot",
  104. Action: engineEnable,
  105. },
  106. {
  107. Name: "list",
  108. Usage: "list available Docker engines (include the Dind engines)",
  109. Flags: []cli.Flag{
  110. cli.BoolFlag{
  111. Name: "update, u",
  112. Usage: "update engine cache",
  113. },
  114. },
  115. Action: engineList,
  116. },
  117. }
  118. }
  119. func engineSwitch(c *cli.Context) error {
  120. if len(c.Args()) != 1 {
  121. log.Fatal("Must specify exactly one Docker engine to switch to")
  122. }
  123. newEngine := c.Args()[0]
  124. cfg := config.LoadConfig()
  125. validateEngine(newEngine, cfg)
  126. project, err := compose.GetProject(cfg, true, false)
  127. if err != nil {
  128. log.Fatal(err)
  129. }
  130. if err = project.Stop(context.Background(), 10, "docker"); err != nil {
  131. log.Fatal(err)
  132. }
  133. if err = compose.LoadSpecialService(project, cfg, "docker", newEngine); err != nil {
  134. log.Fatal(err)
  135. }
  136. if err = project.Up(context.Background(), options.Up{}, "docker"); err != nil {
  137. log.Fatal(err)
  138. }
  139. if err := config.Set("rancher.docker.engine", newEngine); err != nil {
  140. log.Errorf("Failed to update rancher.docker.engine: %v", err)
  141. }
  142. return nil
  143. }
  144. func engineCreate(c *cli.Context) error {
  145. name := c.Args()[0]
  146. version := c.String("version")
  147. sshPort, _ := strconv.Atoi(c.String("ssh-port"))
  148. if sshPort <= 0 {
  149. sshPort = randomSSHPort()
  150. }
  151. authorizedKeys := c.String("authorized-keys")
  152. network := c.String("network")
  153. fixedIP := c.String("fixed-ip")
  154. // generate & create engine compose
  155. err := generateEngineCompose(name, version, sshPort, authorizedKeys, network, fixedIP)
  156. if err != nil {
  157. return err
  158. }
  159. // stage engine service
  160. cfg := config.LoadConfig()
  161. var enabledServices []string
  162. if val, ok := cfg.Rancher.ServicesInclude[name]; !ok || !val {
  163. cfg.Rancher.ServicesInclude[name] = true
  164. enabledServices = append(enabledServices, name)
  165. }
  166. if len(enabledServices) > 0 {
  167. if err := compose.StageServices(cfg, enabledServices...); err != nil {
  168. log.Fatal(err)
  169. }
  170. if err := config.Set("rancher.services_include", cfg.Rancher.ServicesInclude); err != nil {
  171. log.Fatal(err)
  172. }
  173. }
  174. // generate engine script
  175. err = util.GenerateDindEngineScript(name)
  176. if err != nil {
  177. log.Fatal(err)
  178. }
  179. return nil
  180. }
  181. func dindEngineRemove(c *cli.Context) error {
  182. if !c.Bool("force") {
  183. if !yes("Continue") {
  184. return nil
  185. }
  186. }
  187. // app.ProjectDelete needs to use this flag
  188. // Allow deletion of the Dind engine
  189. c.Set("force", "true")
  190. // Remove volumes associated with the Dind engine container
  191. c.Set("v", "true")
  192. name := c.Args()[0]
  193. cfg := config.LoadConfig()
  194. p, err := compose.GetProject(cfg, true, false)
  195. if err != nil {
  196. log.Fatalf("Get project failed: %v", err)
  197. }
  198. // 1. service stop
  199. err = app.ProjectStop(p, c)
  200. if err != nil {
  201. log.Fatalf("Stop project service failed: %v", err)
  202. }
  203. // 2. service delete
  204. err = app.ProjectDelete(p, c)
  205. if err != nil {
  206. log.Fatalf("Delete project service failed: %v", err)
  207. }
  208. // 3. service delete
  209. if _, ok := cfg.Rancher.ServicesInclude[name]; !ok {
  210. log.Fatalf("Failed to found enabled service %s", name)
  211. }
  212. delete(cfg.Rancher.ServicesInclude, name)
  213. if err = config.Set("rancher.services_include", cfg.Rancher.ServicesInclude); err != nil {
  214. log.Fatal(err)
  215. }
  216. // 4. remove service from file
  217. err = RemoveEngineFromCompose(name)
  218. if err != nil {
  219. log.Fatal(err)
  220. }
  221. // 5. remove dind engine script
  222. err = util.RemoveDindEngineScript(name)
  223. if err != nil {
  224. return err
  225. }
  226. return nil
  227. }
  228. func engineEnable(c *cli.Context) error {
  229. if len(c.Args()) != 1 {
  230. log.Fatal("Must specify exactly one Docker engine to enable")
  231. }
  232. newEngine := c.Args()[0]
  233. cfg := config.LoadConfig()
  234. validateEngine(newEngine, cfg)
  235. if err := compose.StageServices(cfg, newEngine); err != nil {
  236. return err
  237. }
  238. if err := config.Set("rancher.docker.engine", newEngine); err != nil {
  239. log.Errorf("Failed to update 'rancher.docker.engine': %v", err)
  240. }
  241. return nil
  242. }
  243. func engineList(c *cli.Context) error {
  244. cfg := config.LoadConfig()
  245. engines := availableEngines(cfg, c.Bool("update"))
  246. currentEngine := CurrentEngine()
  247. for _, engine := range engines {
  248. if engine == currentEngine {
  249. fmt.Printf("current %s\n", engine)
  250. } else if engine == cfg.Rancher.Docker.Engine {
  251. fmt.Printf("enabled %s\n", engine)
  252. } else {
  253. fmt.Printf("disabled %s\n", engine)
  254. }
  255. }
  256. // check the dind container
  257. client, err := docker.NewSystemClient()
  258. if err != nil {
  259. log.Warnf("Failed to detect dind: %v", err)
  260. return nil
  261. }
  262. filter := filters.NewArgs()
  263. filter.Add("label", config.UserDockerLabel)
  264. opts := types.ContainerListOptions{
  265. All: true,
  266. Filter: filter,
  267. }
  268. containers, err := client.ContainerList(context.Background(), opts)
  269. if err != nil {
  270. log.Warnf("Failed to detect dind: %v", err)
  271. return nil
  272. }
  273. for _, c := range containers {
  274. if c.State == "running" {
  275. fmt.Printf("enabled %s\n", c.Labels[config.UserDockerLabel])
  276. } else {
  277. fmt.Printf("disabled %s\n", c.Labels[config.UserDockerLabel])
  278. }
  279. }
  280. return nil
  281. }
  282. func validateEngine(engine string, cfg *config.CloudConfig) {
  283. engines := availableEngines(cfg, false)
  284. if !service.IsLocalOrURL(engine) && !util.Contains(engines, engine) {
  285. log.Fatalf("%s is not a valid engine", engine)
  286. }
  287. }
  288. func availableEngines(cfg *config.CloudConfig, update bool) []string {
  289. if update {
  290. err := network.UpdateCaches(cfg.Rancher.Repositories.ToArray(), "engines")
  291. if err != nil {
  292. log.Debugf("Failed to update engine caches: %v", err)
  293. }
  294. }
  295. engines, err := network.GetEngines(cfg.Rancher.Repositories.ToArray())
  296. if err != nil {
  297. log.Fatal(err)
  298. }
  299. sort.Strings(engines)
  300. return engines
  301. }
  302. // CurrentEngine gets the name of the docker that's running
  303. func CurrentEngine() (engine string) {
  304. // sudo system-docker inspect --format "{{.Config.Image}}" docker
  305. client, err := docker.NewSystemClient()
  306. if err != nil {
  307. log.Warnf("Failed to detect current docker: %v", err)
  308. return
  309. }
  310. info, err := client.ContainerInspect(context.Background(), "docker")
  311. if err != nil {
  312. log.Warnf("Failed to detect current docker: %v", err)
  313. return
  314. }
  315. // parse image name, then remove os- prefix and the engine suffix
  316. image, err := reference.ParseNamed(info.Config.Image)
  317. if err != nil {
  318. log.Warnf("Failed to detect current docker(%s): %v", info.Config.Image, err)
  319. return
  320. }
  321. if t, ok := image.(reference.NamedTagged); ok {
  322. tag := t.Tag()
  323. if !strings.HasPrefix(tag, "1.") {
  324. // TODO: this assumes we only do Docker ce :/
  325. tag = tag + "-ce"
  326. }
  327. return "docker-" + tag
  328. }
  329. return
  330. }
  331. func preFlightValidate(c *cli.Context) error {
  332. if len(c.Args()) != 1 {
  333. return errors.New("Must specify one engine name")
  334. }
  335. name := c.Args()[0]
  336. if name == "" {
  337. return errors.New("Must specify one engine name")
  338. }
  339. version := c.String("version")
  340. if version == "" {
  341. return errors.New("Must specify one engine version")
  342. }
  343. authorizedKeys := c.String("authorized-keys")
  344. if authorizedKeys != "" {
  345. if _, err := os.Stat(authorizedKeys); os.IsNotExist(err) {
  346. return errors.New("The authorized-keys should be an exist file, recommended to put in the /opt or /var/lib/rancher directory")
  347. }
  348. }
  349. network := c.String("network")
  350. if network == "" {
  351. return errors.New("Must specify network")
  352. }
  353. userDefineNetwork, err := CheckUserDefineNetwork(network)
  354. if err != nil {
  355. return err
  356. }
  357. fixedIP := c.String("fixed-ip")
  358. if fixedIP == "" {
  359. return errors.New("Must specify fix ip")
  360. }
  361. err = CheckUserDefineIPv4Address(fixedIP, *userDefineNetwork)
  362. if err != nil {
  363. return err
  364. }
  365. isVersionMatch := false
  366. for _, v := range config.SupportedDinds {
  367. if v == version {
  368. isVersionMatch = true
  369. break
  370. }
  371. }
  372. if !isVersionMatch {
  373. return errors.Errorf("Engine version not supported only %v are supported", config.SupportedDinds)
  374. }
  375. if c.String("ssh-port") != "" {
  376. port, err := strconv.Atoi(c.String("ssh-port"))
  377. if err != nil {
  378. return errors.Wrap(err, "Failed to convert ssh port to Int")
  379. }
  380. if port > 0 {
  381. addr, err := net.ResolveTCPAddr("tcp", "localhost:"+strconv.Itoa(port))
  382. if err != nil {
  383. return errors.Errorf("Failed to resolve tcp addr: %v", err)
  384. }
  385. l, err := net.ListenTCP("tcp", addr)
  386. if err != nil {
  387. return errors.Errorf("Failed to listen tcp: %v", err)
  388. }
  389. defer l.Close()
  390. }
  391. }
  392. return nil
  393. }
  394. func randomSSHPort() int {
  395. addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
  396. if err != nil {
  397. log.Errorf("Failed to resolve tcp addr: %v", err)
  398. return 0
  399. }
  400. l, err := net.ListenTCP("tcp", addr)
  401. if err != nil {
  402. return 0
  403. }
  404. defer l.Close()
  405. return l.Addr().(*net.TCPAddr).Port
  406. }
  407. func generateEngineCompose(name, version string, sshPort int, authorizedKeys, network, fixedIP string) error {
  408. if err := os.MkdirAll(path.Dir(config.MultiDockerConfFile), 0700); err != nil && !os.IsExist(err) {
  409. log.Errorf("Failed to create directory for file %s: %v", config.MultiDockerConfFile, err)
  410. return err
  411. }
  412. composeConfigs := map[string]composeConfig.ServiceConfigV1{}
  413. if _, err := os.Stat(config.MultiDockerConfFile); err == nil {
  414. // read from engine compose
  415. bytes, err := ioutil.ReadFile(config.MultiDockerConfFile)
  416. if err != nil {
  417. return err
  418. }
  419. err = yaml.Unmarshal(bytes, &composeConfigs)
  420. if err != nil {
  421. return err
  422. }
  423. }
  424. if err := os.MkdirAll(config.MultiDockerDataDir+"/"+name, 0700); err != nil && !os.IsExist(err) {
  425. log.Errorf("Failed to create directory for file %s: %v", config.MultiDockerDataDir+"/"+name, err)
  426. return err
  427. }
  428. volumes := []string{
  429. "/lib/modules:/lib/modules",
  430. config.MultiDockerDataDir + "/" + name + ":" + config.MultiDockerDataDir + "/" + name,
  431. }
  432. if authorizedKeys != "" {
  433. volumes = append(volumes, authorizedKeys+":/root/.ssh/authorized_keys")
  434. }
  435. composeConfigs[name] = composeConfig.ServiceConfigV1{
  436. Image: "${REGISTRY_DOMAIN}/" + version,
  437. Restart: "always",
  438. Privileged: true,
  439. Net: network,
  440. Ports: []string{strconv.Itoa(sshPort) + ":22"},
  441. Volumes: volumes,
  442. VolumesFrom: []string{},
  443. Command: composeYaml.Command{
  444. "--storage-driver=overlay2",
  445. "--data-root=" + config.MultiDockerDataDir + "/" + name,
  446. "--host=unix://" + config.MultiDockerDataDir + "/" + name + "/docker-" + name + ".sock",
  447. },
  448. Labels: composeYaml.SliceorMap{
  449. "io.rancher.os.scope": "system",
  450. "io.rancher.os.after": "console",
  451. config.UserDockerLabel: name,
  452. config.UserDockerNetLabel: network,
  453. config.UserDockerFIPLabel: fixedIP,
  454. },
  455. }
  456. bytes, err := yaml.Marshal(composeConfigs)
  457. if err != nil {
  458. return err
  459. }
  460. return ioutil.WriteFile(config.MultiDockerConfFile, bytes, 0640)
  461. }
  462. func RemoveEngineFromCompose(name string) error {
  463. composeConfigs := map[string]composeConfig.ServiceConfigV1{}
  464. if _, err := os.Stat(config.MultiDockerConfFile); err == nil {
  465. // read from engine compose
  466. bytes, err := ioutil.ReadFile(config.MultiDockerConfFile)
  467. if err != nil {
  468. return err
  469. }
  470. err = yaml.Unmarshal(bytes, &composeConfigs)
  471. if err != nil {
  472. return err
  473. }
  474. }
  475. delete(composeConfigs, name)
  476. bytes, err := yaml.Marshal(composeConfigs)
  477. if err != nil {
  478. return err
  479. }
  480. return ioutil.WriteFile(config.MultiDockerConfFile, bytes, 0640)
  481. }
  482. func CheckUserDefineNetwork(name string) (*types.NetworkResource, error) {
  483. systemClient, err := docker.NewSystemClient()
  484. if err != nil {
  485. return nil, err
  486. }
  487. networks, err := systemClient.NetworkList(context.Background(), types.NetworkListOptions{})
  488. if err != nil {
  489. return nil, err
  490. }
  491. for _, network := range networks {
  492. if network.Name == name {
  493. return &network, nil
  494. }
  495. }
  496. return nil, errors.Errorf("Failed to found the user define network: %s", name)
  497. }
  498. func CheckUserDefineIPv4Address(ipv4 string, network types.NetworkResource) error {
  499. for _, config := range network.IPAM.Config {
  500. _, ipnet, _ := net.ParseCIDR(config.Subnet)
  501. if ipnet.Contains(net.ParseIP(ipv4)) {
  502. return nil
  503. }
  504. }
  505. return errors.Errorf("IP %s is not in the specified cidr", ipv4)
  506. }