engine.go 14 KB

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