github.com/noxiouz/docker@v0.7.3-0.20160629055221-3d231c78e8c5/daemon/oci_linux.go (about) 1 package daemon 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "sort" 9 "strconv" 10 "strings" 11 12 "github.com/Sirupsen/logrus" 13 "github.com/docker/docker/container" 14 "github.com/docker/docker/daemon/caps" 15 "github.com/docker/docker/libcontainerd" 16 "github.com/docker/docker/oci" 17 "github.com/docker/docker/pkg/idtools" 18 "github.com/docker/docker/pkg/mount" 19 "github.com/docker/docker/pkg/stringutils" 20 "github.com/docker/docker/pkg/symlink" 21 "github.com/docker/docker/volume" 22 containertypes "github.com/docker/engine-api/types/container" 23 "github.com/opencontainers/runc/libcontainer/apparmor" 24 "github.com/opencontainers/runc/libcontainer/devices" 25 "github.com/opencontainers/runc/libcontainer/user" 26 "github.com/opencontainers/specs/specs-go" 27 ) 28 29 func setResources(s *specs.Spec, r containertypes.Resources) error { 30 weightDevices, err := getBlkioWeightDevices(r) 31 if err != nil { 32 return err 33 } 34 readBpsDevice, err := getBlkioThrottleDevices(r.BlkioDeviceReadBps) 35 if err != nil { 36 return err 37 } 38 writeBpsDevice, err := getBlkioThrottleDevices(r.BlkioDeviceWriteBps) 39 if err != nil { 40 return err 41 } 42 readIOpsDevice, err := getBlkioThrottleDevices(r.BlkioDeviceReadIOps) 43 if err != nil { 44 return err 45 } 46 writeIOpsDevice, err := getBlkioThrottleDevices(r.BlkioDeviceWriteIOps) 47 if err != nil { 48 return err 49 } 50 51 memoryRes := getMemoryResources(r) 52 cpuRes := getCPUResources(r) 53 blkioWeight := r.BlkioWeight 54 55 specResources := &specs.Resources{ 56 Memory: memoryRes, 57 CPU: cpuRes, 58 BlockIO: &specs.BlockIO{ 59 Weight: &blkioWeight, 60 WeightDevice: weightDevices, 61 ThrottleReadBpsDevice: readBpsDevice, 62 ThrottleWriteBpsDevice: writeBpsDevice, 63 ThrottleReadIOPSDevice: readIOpsDevice, 64 ThrottleWriteIOPSDevice: writeIOpsDevice, 65 }, 66 DisableOOMKiller: r.OomKillDisable, 67 Pids: &specs.Pids{ 68 Limit: &r.PidsLimit, 69 }, 70 } 71 72 if s.Linux.Resources != nil && len(s.Linux.Resources.Devices) > 0 { 73 specResources.Devices = s.Linux.Resources.Devices 74 } 75 76 s.Linux.Resources = specResources 77 return nil 78 } 79 80 func setDevices(s *specs.Spec, c *container.Container) error { 81 // Build lists of devices allowed and created within the container. 82 var devs []specs.Device 83 devPermissions := s.Linux.Resources.Devices 84 if c.HostConfig.Privileged { 85 hostDevices, err := devices.HostDevices() 86 if err != nil { 87 return err 88 } 89 for _, d := range hostDevices { 90 devs = append(devs, specDevice(d)) 91 } 92 rwm := "rwm" 93 devPermissions = []specs.DeviceCgroup{ 94 { 95 Allow: true, 96 Access: &rwm, 97 }, 98 } 99 } else { 100 for _, deviceMapping := range c.HostConfig.Devices { 101 d, dPermissions, err := getDevicesFromPath(deviceMapping) 102 if err != nil { 103 return err 104 } 105 devs = append(devs, d...) 106 devPermissions = append(devPermissions, dPermissions...) 107 } 108 } 109 110 s.Linux.Devices = append(s.Linux.Devices, devs...) 111 s.Linux.Resources.Devices = devPermissions 112 return nil 113 } 114 115 func setRlimits(daemon *Daemon, s *specs.Spec, c *container.Container) error { 116 var rlimits []specs.Rlimit 117 118 ulimits := c.HostConfig.Ulimits 119 // Merge ulimits with daemon defaults 120 ulIdx := make(map[string]struct{}) 121 for _, ul := range ulimits { 122 ulIdx[ul.Name] = struct{}{} 123 } 124 for name, ul := range daemon.configStore.Ulimits { 125 if _, exists := ulIdx[name]; !exists { 126 ulimits = append(ulimits, ul) 127 } 128 } 129 130 for _, ul := range ulimits { 131 rlimits = append(rlimits, specs.Rlimit{ 132 Type: "RLIMIT_" + strings.ToUpper(ul.Name), 133 Soft: uint64(ul.Soft), 134 Hard: uint64(ul.Hard), 135 }) 136 } 137 138 s.Process.Rlimits = rlimits 139 return nil 140 } 141 142 func setUser(s *specs.Spec, c *container.Container) error { 143 uid, gid, additionalGids, err := getUser(c, c.Config.User) 144 if err != nil { 145 return err 146 } 147 s.Process.User.UID = uid 148 s.Process.User.GID = gid 149 s.Process.User.AdditionalGids = additionalGids 150 return nil 151 } 152 153 func readUserFile(c *container.Container, p string) (io.ReadCloser, error) { 154 fp, err := symlink.FollowSymlinkInScope(filepath.Join(c.BaseFS, p), c.BaseFS) 155 if err != nil { 156 return nil, err 157 } 158 return os.Open(fp) 159 } 160 161 func getUser(c *container.Container, username string) (uint32, uint32, []uint32, error) { 162 passwdPath, err := user.GetPasswdPath() 163 if err != nil { 164 return 0, 0, nil, err 165 } 166 groupPath, err := user.GetGroupPath() 167 if err != nil { 168 return 0, 0, nil, err 169 } 170 passwdFile, err := readUserFile(c, passwdPath) 171 if err == nil { 172 defer passwdFile.Close() 173 } 174 groupFile, err := readUserFile(c, groupPath) 175 if err == nil { 176 defer groupFile.Close() 177 } 178 179 execUser, err := user.GetExecUser(username, nil, passwdFile, groupFile) 180 if err != nil { 181 return 0, 0, nil, err 182 } 183 184 // todo: fix this double read by a change to libcontainer/user pkg 185 groupFile, err = readUserFile(c, groupPath) 186 if err == nil { 187 defer groupFile.Close() 188 } 189 var addGroups []int 190 if len(c.HostConfig.GroupAdd) > 0 { 191 addGroups, err = user.GetAdditionalGroups(c.HostConfig.GroupAdd, groupFile) 192 if err != nil { 193 return 0, 0, nil, err 194 } 195 } 196 uid := uint32(execUser.Uid) 197 gid := uint32(execUser.Gid) 198 sgids := append(execUser.Sgids, addGroups...) 199 var additionalGids []uint32 200 for _, g := range sgids { 201 additionalGids = append(additionalGids, uint32(g)) 202 } 203 return uid, gid, additionalGids, nil 204 } 205 206 func setNamespace(s *specs.Spec, ns specs.Namespace) { 207 for i, n := range s.Linux.Namespaces { 208 if n.Type == ns.Type { 209 s.Linux.Namespaces[i] = ns 210 return 211 } 212 } 213 s.Linux.Namespaces = append(s.Linux.Namespaces, ns) 214 } 215 216 func setCapabilities(s *specs.Spec, c *container.Container) error { 217 var caplist []string 218 var err error 219 if c.HostConfig.Privileged { 220 caplist = caps.GetAllCapabilities() 221 } else { 222 caplist, err = caps.TweakCapabilities(s.Process.Capabilities, c.HostConfig.CapAdd, c.HostConfig.CapDrop) 223 if err != nil { 224 return err 225 } 226 } 227 s.Process.Capabilities = caplist 228 return nil 229 } 230 231 func delNamespace(s *specs.Spec, nsType specs.NamespaceType) { 232 idx := -1 233 for i, n := range s.Linux.Namespaces { 234 if n.Type == nsType { 235 idx = i 236 } 237 } 238 if idx >= 0 { 239 s.Linux.Namespaces = append(s.Linux.Namespaces[:idx], s.Linux.Namespaces[idx+1:]...) 240 } 241 } 242 243 func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error { 244 userNS := false 245 // user 246 if c.HostConfig.UsernsMode.IsPrivate() { 247 uidMap, gidMap := daemon.GetUIDGIDMaps() 248 if uidMap != nil { 249 userNS = true 250 ns := specs.Namespace{Type: "user"} 251 setNamespace(s, ns) 252 s.Linux.UIDMappings = specMapping(uidMap) 253 s.Linux.GIDMappings = specMapping(gidMap) 254 } 255 } 256 // network 257 if !c.Config.NetworkDisabled { 258 ns := specs.Namespace{Type: "network"} 259 parts := strings.SplitN(string(c.HostConfig.NetworkMode), ":", 2) 260 if parts[0] == "container" { 261 nc, err := daemon.getNetworkedContainer(c.ID, c.HostConfig.NetworkMode.ConnectedContainer()) 262 if err != nil { 263 return err 264 } 265 ns.Path = fmt.Sprintf("/proc/%d/ns/net", nc.State.GetPID()) 266 if userNS { 267 // to share a net namespace, they must also share a user namespace 268 nsUser := specs.Namespace{Type: "user"} 269 nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", nc.State.GetPID()) 270 setNamespace(s, nsUser) 271 } 272 } else if c.HostConfig.NetworkMode.IsHost() { 273 ns.Path = c.NetworkSettings.SandboxKey 274 } 275 setNamespace(s, ns) 276 } 277 // ipc 278 if c.HostConfig.IpcMode.IsContainer() { 279 ns := specs.Namespace{Type: "ipc"} 280 ic, err := daemon.getIpcContainer(c) 281 if err != nil { 282 return err 283 } 284 ns.Path = fmt.Sprintf("/proc/%d/ns/ipc", ic.State.GetPID()) 285 setNamespace(s, ns) 286 if userNS { 287 // to share an IPC namespace, they must also share a user namespace 288 nsUser := specs.Namespace{Type: "user"} 289 nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", ic.State.GetPID()) 290 setNamespace(s, nsUser) 291 } 292 } else if c.HostConfig.IpcMode.IsHost() { 293 delNamespace(s, specs.NamespaceType("ipc")) 294 } else { 295 ns := specs.Namespace{Type: "ipc"} 296 setNamespace(s, ns) 297 } 298 // pid 299 if c.HostConfig.PidMode.IsContainer() { 300 ns := specs.Namespace{Type: "pid"} 301 pc, err := daemon.getPidContainer(c) 302 if err != nil { 303 return err 304 } 305 ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID()) 306 setNamespace(s, ns) 307 if userNS { 308 // to share a PID namespace, they must also share a user namespace 309 nsUser := specs.Namespace{Type: "user"} 310 nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", pc.State.GetPID()) 311 setNamespace(s, nsUser) 312 } 313 } else if c.HostConfig.PidMode.IsHost() { 314 delNamespace(s, specs.NamespaceType("pid")) 315 } else { 316 ns := specs.Namespace{Type: "pid"} 317 setNamespace(s, ns) 318 } 319 // uts 320 if c.HostConfig.UTSMode.IsHost() { 321 delNamespace(s, specs.NamespaceType("uts")) 322 s.Hostname = "" 323 } 324 325 return nil 326 } 327 328 func specMapping(s []idtools.IDMap) []specs.IDMapping { 329 var ids []specs.IDMapping 330 for _, item := range s { 331 ids = append(ids, specs.IDMapping{ 332 HostID: uint32(item.HostID), 333 ContainerID: uint32(item.ContainerID), 334 Size: uint32(item.Size), 335 }) 336 } 337 return ids 338 } 339 340 func getMountInfo(mountinfo []*mount.Info, dir string) *mount.Info { 341 for _, m := range mountinfo { 342 if m.Mountpoint == dir { 343 return m 344 } 345 } 346 return nil 347 } 348 349 // Get the source mount point of directory passed in as argument. Also return 350 // optional fields. 351 func getSourceMount(source string) (string, string, error) { 352 // Ensure any symlinks are resolved. 353 sourcePath, err := filepath.EvalSymlinks(source) 354 if err != nil { 355 return "", "", err 356 } 357 358 mountinfos, err := mount.GetMounts() 359 if err != nil { 360 return "", "", err 361 } 362 363 mountinfo := getMountInfo(mountinfos, sourcePath) 364 if mountinfo != nil { 365 return sourcePath, mountinfo.Optional, nil 366 } 367 368 path := sourcePath 369 for { 370 path = filepath.Dir(path) 371 372 mountinfo = getMountInfo(mountinfos, path) 373 if mountinfo != nil { 374 return path, mountinfo.Optional, nil 375 } 376 377 if path == "/" { 378 break 379 } 380 } 381 382 // If we are here, we did not find parent mount. Something is wrong. 383 return "", "", fmt.Errorf("Could not find source mount of %s", source) 384 } 385 386 // Ensure mount point on which path is mounted, is shared. 387 func ensureShared(path string) error { 388 sharedMount := false 389 390 sourceMount, optionalOpts, err := getSourceMount(path) 391 if err != nil { 392 return err 393 } 394 // Make sure source mount point is shared. 395 optsSplit := strings.Split(optionalOpts, " ") 396 for _, opt := range optsSplit { 397 if strings.HasPrefix(opt, "shared:") { 398 sharedMount = true 399 break 400 } 401 } 402 403 if !sharedMount { 404 return fmt.Errorf("Path %s is mounted on %s but it is not a shared mount.", path, sourceMount) 405 } 406 return nil 407 } 408 409 // Ensure mount point on which path is mounted, is either shared or slave. 410 func ensureSharedOrSlave(path string) error { 411 sharedMount := false 412 slaveMount := false 413 414 sourceMount, optionalOpts, err := getSourceMount(path) 415 if err != nil { 416 return err 417 } 418 // Make sure source mount point is shared. 419 optsSplit := strings.Split(optionalOpts, " ") 420 for _, opt := range optsSplit { 421 if strings.HasPrefix(opt, "shared:") { 422 sharedMount = true 423 break 424 } else if strings.HasPrefix(opt, "master:") { 425 slaveMount = true 426 break 427 } 428 } 429 430 if !sharedMount && !slaveMount { 431 return fmt.Errorf("Path %s is mounted on %s but it is not a shared or slave mount.", path, sourceMount) 432 } 433 return nil 434 } 435 436 var ( 437 mountPropagationMap = map[string]int{ 438 "private": mount.PRIVATE, 439 "rprivate": mount.RPRIVATE, 440 "shared": mount.SHARED, 441 "rshared": mount.RSHARED, 442 "slave": mount.SLAVE, 443 "rslave": mount.RSLAVE, 444 } 445 446 mountPropagationReverseMap = map[int]string{ 447 mount.PRIVATE: "private", 448 mount.RPRIVATE: "rprivate", 449 mount.SHARED: "shared", 450 mount.RSHARED: "rshared", 451 mount.SLAVE: "slave", 452 mount.RSLAVE: "rslave", 453 } 454 ) 455 456 func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []container.Mount) error { 457 userMounts := make(map[string]struct{}) 458 for _, m := range mounts { 459 userMounts[m.Destination] = struct{}{} 460 } 461 462 // Filter out mounts that are overridden by user supplied mounts 463 var defaultMounts []specs.Mount 464 _, mountDev := userMounts["/dev"] 465 for _, m := range s.Mounts { 466 if _, ok := userMounts[m.Destination]; !ok { 467 if mountDev && strings.HasPrefix(m.Destination, "/dev/") { 468 continue 469 } 470 defaultMounts = append(defaultMounts, m) 471 } 472 } 473 474 s.Mounts = defaultMounts 475 for _, m := range mounts { 476 for _, cm := range s.Mounts { 477 if cm.Destination == m.Destination { 478 return fmt.Errorf("Duplicate mount point '%s'", m.Destination) 479 } 480 } 481 482 if m.Source == "tmpfs" { 483 data := c.HostConfig.Tmpfs[m.Destination] 484 options := []string{"noexec", "nosuid", "nodev", volume.DefaultPropagationMode} 485 if data != "" { 486 options = append(options, strings.Split(data, ",")...) 487 } 488 489 merged, err := mount.MergeTmpfsOptions(options) 490 if err != nil { 491 return err 492 } 493 494 s.Mounts = append(s.Mounts, specs.Mount{Destination: m.Destination, Source: m.Source, Type: "tmpfs", Options: merged}) 495 continue 496 } 497 498 mt := specs.Mount{Destination: m.Destination, Source: m.Source, Type: "bind"} 499 500 // Determine property of RootPropagation based on volume 501 // properties. If a volume is shared, then keep root propagation 502 // shared. This should work for slave and private volumes too. 503 // 504 // For slave volumes, it can be either [r]shared/[r]slave. 505 // 506 // For private volumes any root propagation value should work. 507 pFlag := mountPropagationMap[m.Propagation] 508 if pFlag == mount.SHARED || pFlag == mount.RSHARED { 509 if err := ensureShared(m.Source); err != nil { 510 return err 511 } 512 rootpg := mountPropagationMap[s.Linux.RootfsPropagation] 513 if rootpg != mount.SHARED && rootpg != mount.RSHARED { 514 s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.SHARED] 515 } 516 } else if pFlag == mount.SLAVE || pFlag == mount.RSLAVE { 517 if err := ensureSharedOrSlave(m.Source); err != nil { 518 return err 519 } 520 rootpg := mountPropagationMap[s.Linux.RootfsPropagation] 521 if rootpg != mount.SHARED && rootpg != mount.RSHARED && rootpg != mount.SLAVE && rootpg != mount.RSLAVE { 522 s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.RSLAVE] 523 } 524 } 525 526 opts := []string{"rbind"} 527 if !m.Writable { 528 opts = append(opts, "ro") 529 } 530 if pFlag != 0 { 531 opts = append(opts, mountPropagationReverseMap[pFlag]) 532 } 533 534 mt.Options = opts 535 s.Mounts = append(s.Mounts, mt) 536 } 537 538 if s.Root.Readonly { 539 for i, m := range s.Mounts { 540 switch m.Destination { 541 case "/proc", "/dev/pts", "/dev/mqueue": // /dev is remounted by runc 542 continue 543 } 544 if _, ok := userMounts[m.Destination]; !ok { 545 if !stringutils.InSlice(m.Options, "ro") { 546 s.Mounts[i].Options = append(s.Mounts[i].Options, "ro") 547 } 548 } 549 } 550 } 551 552 if c.HostConfig.Privileged { 553 if !s.Root.Readonly { 554 // clear readonly for /sys 555 for i := range s.Mounts { 556 if s.Mounts[i].Destination == "/sys" { 557 clearReadOnly(&s.Mounts[i]) 558 } 559 } 560 } 561 s.Linux.ReadonlyPaths = nil 562 s.Linux.MaskedPaths = nil 563 } 564 565 // TODO: until a kernel/mount solution exists for handling remount in a user namespace, 566 // we must clear the readonly flag for the cgroups mount (@mrunalp concurs) 567 if uidMap, _ := daemon.GetUIDGIDMaps(); uidMap != nil || c.HostConfig.Privileged { 568 for i, m := range s.Mounts { 569 if m.Type == "cgroup" { 570 clearReadOnly(&s.Mounts[i]) 571 } 572 } 573 } 574 575 return nil 576 } 577 578 func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container) error { 579 linkedEnv, err := daemon.setupLinkedContainers(c) 580 if err != nil { 581 return err 582 } 583 s.Root = specs.Root{ 584 Path: c.BaseFS, 585 Readonly: c.HostConfig.ReadonlyRootfs, 586 } 587 rootUID, rootGID := daemon.GetRemappedUIDGID() 588 if err := c.SetupWorkingDirectory(rootUID, rootGID); err != nil { 589 return err 590 } 591 cwd := c.Config.WorkingDir 592 if len(cwd) == 0 { 593 cwd = "/" 594 } 595 s.Process.Args = append([]string{c.Path}, c.Args...) 596 s.Process.Cwd = cwd 597 s.Process.Env = c.CreateDaemonEnvironment(linkedEnv) 598 s.Process.Terminal = c.Config.Tty 599 s.Hostname = c.FullHostname() 600 601 return nil 602 } 603 604 func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, error) { 605 s := oci.DefaultSpec() 606 if err := daemon.populateCommonSpec(&s, c); err != nil { 607 return nil, err 608 } 609 610 var cgroupsPath string 611 scopePrefix := "docker" 612 parent := "/docker" 613 useSystemd := UsingSystemd(daemon.configStore) 614 if useSystemd { 615 parent = "system.slice" 616 } 617 618 if c.HostConfig.CgroupParent != "" { 619 parent = c.HostConfig.CgroupParent 620 } else if daemon.configStore.CgroupParent != "" { 621 parent = daemon.configStore.CgroupParent 622 } 623 624 if useSystemd { 625 cgroupsPath = parent + ":" + scopePrefix + ":" + c.ID 626 logrus.Debugf("createSpec: cgroupsPath: %s", cgroupsPath) 627 } else { 628 cgroupsPath = filepath.Join(parent, c.ID) 629 } 630 s.Linux.CgroupsPath = &cgroupsPath 631 632 if err := setResources(&s, c.HostConfig.Resources); err != nil { 633 return nil, fmt.Errorf("linux runtime spec resources: %v", err) 634 } 635 s.Linux.Resources.OOMScoreAdj = &c.HostConfig.OomScoreAdj 636 s.Linux.Sysctl = c.HostConfig.Sysctls 637 if err := setDevices(&s, c); err != nil { 638 return nil, fmt.Errorf("linux runtime spec devices: %v", err) 639 } 640 if err := setRlimits(daemon, &s, c); err != nil { 641 return nil, fmt.Errorf("linux runtime spec rlimits: %v", err) 642 } 643 if err := setUser(&s, c); err != nil { 644 return nil, fmt.Errorf("linux spec user: %v", err) 645 } 646 if err := setNamespaces(daemon, &s, c); err != nil { 647 return nil, fmt.Errorf("linux spec namespaces: %v", err) 648 } 649 if err := setCapabilities(&s, c); err != nil { 650 return nil, fmt.Errorf("linux spec capabilities: %v", err) 651 } 652 if err := setSeccomp(daemon, &s, c); err != nil { 653 return nil, fmt.Errorf("linux seccomp: %v", err) 654 } 655 656 if err := daemon.setupIpcDirs(c); err != nil { 657 return nil, err 658 } 659 660 ms, err := daemon.setupMounts(c) 661 if err != nil { 662 return nil, err 663 } 664 ms = append(ms, c.IpcMounts()...) 665 ms = append(ms, c.TmpfsMounts()...) 666 sort.Sort(mounts(ms)) 667 if err := setMounts(daemon, &s, c, ms); err != nil { 668 return nil, fmt.Errorf("linux mounts: %v", err) 669 } 670 671 for _, ns := range s.Linux.Namespaces { 672 if ns.Type == "network" && ns.Path == "" && !c.Config.NetworkDisabled { 673 target, err := os.Readlink(filepath.Join("/proc", strconv.Itoa(os.Getpid()), "exe")) 674 if err != nil { 675 return nil, err 676 } 677 678 s.Hooks = specs.Hooks{ 679 Prestart: []specs.Hook{{ 680 Path: target, // FIXME: cross-platform 681 Args: []string{"libnetwork-setkey", c.ID, daemon.netController.ID()}, 682 }}, 683 } 684 } 685 } 686 687 if apparmor.IsEnabled() { 688 appArmorProfile := "docker-default" 689 if len(c.AppArmorProfile) > 0 { 690 appArmorProfile = c.AppArmorProfile 691 } else if c.HostConfig.Privileged { 692 appArmorProfile = "unconfined" 693 } 694 s.Process.ApparmorProfile = appArmorProfile 695 } 696 s.Process.SelinuxLabel = c.GetProcessLabel() 697 s.Process.NoNewPrivileges = c.NoNewPrivileges 698 s.Linux.MountLabel = c.MountLabel 699 700 return (*libcontainerd.Spec)(&s), nil 701 } 702 703 func clearReadOnly(m *specs.Mount) { 704 var opt []string 705 for _, o := range m.Options { 706 if o != "ro" { 707 opt = append(opt, o) 708 } 709 } 710 m.Options = opt 711 }