mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	pids limit support
update bash commpletion for pids limit update check config for kernel add docs for pids limit add pids stats add stats to docker client Signed-off-by: Jessica Frazelle <acidburn@docker.com>
This commit is contained in:
		
							parent
							
								
									9e2c4de0de
								
							
						
					
					
						commit
						69cf03700f
					
				
					 21 changed files with 83 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -164,7 +164,7 @@ func (cli *DockerCli) CmdStats(args ...string) error {
 | 
			
		|||
			fmt.Fprint(cli.out, "\033[2J")
 | 
			
		||||
			fmt.Fprint(cli.out, "\033[H")
 | 
			
		||||
		}
 | 
			
		||||
		io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\n")
 | 
			
		||||
		io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for range time.Tick(500 * time.Millisecond) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ type containerStats struct {
 | 
			
		|||
	NetworkTx        float64
 | 
			
		||||
	BlockRead        float64
 | 
			
		||||
	BlockWrite       float64
 | 
			
		||||
	PidsCurrent      uint64
 | 
			
		||||
	mu               sync.RWMutex
 | 
			
		||||
	err              error
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -167,13 +168,14 @@ func (s *containerStats) Display(w io.Writer) error {
 | 
			
		|||
	if s.err != nil {
 | 
			
		||||
		return s.err
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Fprintf(w, "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\n",
 | 
			
		||||
	fmt.Fprintf(w, "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n",
 | 
			
		||||
		s.Name,
 | 
			
		||||
		s.CPUPercentage,
 | 
			
		||||
		units.HumanSize(s.Memory), units.HumanSize(s.MemoryLimit),
 | 
			
		||||
		s.MemoryPercentage,
 | 
			
		||||
		units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx),
 | 
			
		||||
		units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite))
 | 
			
		||||
		units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite),
 | 
			
		||||
		s.PidsCurrent)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,7 @@ func TestDisplay(t *testing.T) {
 | 
			
		|||
		NetworkTx:        800 * 1024 * 1024,
 | 
			
		||||
		BlockRead:        100 * 1024 * 1024,
 | 
			
		||||
		BlockWrite:       800 * 1024 * 1024,
 | 
			
		||||
		PidsCurrent:      1,
 | 
			
		||||
		mu:               sync.RWMutex{},
 | 
			
		||||
	}
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +27,7 @@ func TestDisplay(t *testing.T) {
 | 
			
		|||
		t.Fatalf("c.Display() gave error: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	got := b.String()
 | 
			
		||||
	want := "app\t30.00%\t104.9 MB / 2.147 GB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\n"
 | 
			
		||||
	want := "app\t30.00%\t104.9 MB / 2.147 GB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n"
 | 
			
		||||
	if got != want {
 | 
			
		||||
		t.Fatalf("c.Display() = %q, want %q", got, want)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -202,6 +202,9 @@ echo 'Optional Features:'
 | 
			
		|||
{
 | 
			
		||||
	check_flags SECCOMP
 | 
			
		||||
}
 | 
			
		||||
{
 | 
			
		||||
	check_flags CGROUP_PIDS
 | 
			
		||||
}
 | 
			
		||||
{
 | 
			
		||||
	check_flags MEMCG_KMEM MEMCG_SWAP MEMCG_SWAP_ENABLED
 | 
			
		||||
	if  is_set MEMCG_SWAP && ! is_set MEMCG_SWAP_ENABLED; then
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1637,6 +1637,7 @@ _docker_run() {
 | 
			
		|||
		--net-alias
 | 
			
		||||
		--oom-score-adj
 | 
			
		||||
		--pid
 | 
			
		||||
		--pids-limit
 | 
			
		||||
		--publish -p
 | 
			
		||||
		--restart
 | 
			
		||||
		--security-opt
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -534,6 +534,7 @@ __docker_subcommand() {
 | 
			
		|||
        "($help)*--net-alias=[Add network-scoped alias for the container]:alias: "
 | 
			
		||||
        "($help)--oom-kill-disable[Disable OOM Killer]"
 | 
			
		||||
        "($help)--oom-score-adj[Tune the host's OOM preferences for containers (accepts -1000 to 1000)]"
 | 
			
		||||
        "($help)--pids-limit[Tune container pids limit (set -1 for unlimited)]"
 | 
			
		||||
        "($help -P --publish-all)"{-P,--publish-all}"[Publish all exposed ports]"
 | 
			
		||||
        "($help)*"{-p=,--publish=}"[Expose a container's port to the host]:port:_ports"
 | 
			
		||||
        "($help)--pid=[PID namespace to use]:PID: "
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -198,6 +198,7 @@ func (daemon *Daemon) populateCommand(c *container.Container, env []string) erro
 | 
			
		|||
		BlkioThrottleWriteBpsDevice:  writeBpsDevice,
 | 
			
		||||
		BlkioThrottleReadIOpsDevice:  readIOpsDevice,
 | 
			
		||||
		BlkioThrottleWriteIOpsDevice: writeIOpsDevice,
 | 
			
		||||
		PidsLimit:                    c.HostConfig.PidsLimit,
 | 
			
		||||
		MemorySwappiness:             -1,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -285,6 +285,12 @@ func verifyContainerResources(resources *containertypes.Resources, sysInfo *sysi
 | 
			
		|||
		resources.OomKillDisable = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resources.PidsLimit != 0 && !sysInfo.PidsLimit {
 | 
			
		||||
		warnings = append(warnings, "Your kernel does not support pids limit capabilities, pids limit discarded.")
 | 
			
		||||
		logrus.Warnf("Your kernel does not support pids limit capabilities, pids limit discarded.")
 | 
			
		||||
		resources.PidsLimit = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// cpu subsystem checks and adjustments
 | 
			
		||||
	if resources.CPUShares > 0 && !sysInfo.CPUShares {
 | 
			
		||||
		warnings = append(warnings, "Your kernel does not support CPU shares. Shares discarded.")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ type Resources struct {
 | 
			
		|||
	CPUPeriod                    int64                      `json:"cpu_period"`
 | 
			
		||||
	Rlimits                      []*units.Rlimit            `json:"rlimits"`
 | 
			
		||||
	OomKillDisable               bool                       `json:"oom_kill_disable"`
 | 
			
		||||
	PidsLimit                    int64                      `json:"pids_limit"`
 | 
			
		||||
	MemorySwappiness             int64                      `json:"memory_swappiness"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +202,7 @@ func SetupCgroups(container *configs.Config, c *Command) error {
 | 
			
		|||
		container.Cgroups.Resources.BlkioThrottleReadIOPSDevice = c.Resources.BlkioThrottleReadIOpsDevice
 | 
			
		||||
		container.Cgroups.Resources.BlkioThrottleWriteIOPSDevice = c.Resources.BlkioThrottleWriteIOpsDevice
 | 
			
		||||
		container.Cgroups.Resources.OomKillDisable = c.Resources.OomKillDisable
 | 
			
		||||
		container.Cgroups.Resources.PidsLimit = c.Resources.PidsLimit
 | 
			
		||||
		container.Cgroups.Resources.MemorySwappiness = c.Resources.MemorySwappiness
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,10 @@ func convertStatsToAPITypes(ls *libcontainer.Stats) *types.StatsJSON {
 | 
			
		|||
			Stats:    mem.Stats,
 | 
			
		||||
			Failcnt:  mem.Usage.Failcnt,
 | 
			
		||||
		}
 | 
			
		||||
		pids := cs.PidsStats
 | 
			
		||||
		s.PidsStats = types.PidsStats{
 | 
			
		||||
			Current: pids.Current,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,6 +123,8 @@ This section lists each version from latest to oldest.  Each listing includes a
 | 
			
		|||
* `POST /networks/create` now supports enabling ipv6 on the network by setting the `EnableIPv6` field (doing this with a label will no longer work).
 | 
			
		||||
* `GET /info` now returns `CgroupDriver` field showing what cgroup driver the daemon is using; `cgroupfs` or `systemd`.
 | 
			
		||||
* `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported.
 | 
			
		||||
* `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported.
 | 
			
		||||
* `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported.
 | 
			
		||||
 | 
			
		||||
### v1.22 API changes
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -346,6 +346,7 @@ Json Parameters:
 | 
			
		|||
-   **MemorySwappiness** - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100.
 | 
			
		||||
-   **OomKillDisable** - Boolean value, whether to disable OOM Killer for the container or not.
 | 
			
		||||
-   **OomScoreAdj** - An integer value containing the score given to the container in order to tune OOM killer preferences.
 | 
			
		||||
-   **PidsLimit** - Tune a container's pids limit. Set -1 for unlimited.
 | 
			
		||||
-   **AttachStdin** - Boolean value, attaches to `stdin`.
 | 
			
		||||
-   **AttachStdout** - Boolean value, attaches to `stdout`.
 | 
			
		||||
-   **AttachStderr** - Boolean value, attaches to `stderr`.
 | 
			
		||||
| 
						 | 
				
			
			@ -823,6 +824,9 @@ This endpoint returns a live stream of a container's resource usage statistics.
 | 
			
		|||
 | 
			
		||||
      {
 | 
			
		||||
         "read" : "2015-01-08T22:57:31.547920715Z",
 | 
			
		||||
         "pids_stats": {
 | 
			
		||||
            "current": 3
 | 
			
		||||
         },
 | 
			
		||||
         "networks": {
 | 
			
		||||
                 "eth0": {
 | 
			
		||||
                     "rx_bytes": 5338,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,6 +74,7 @@ Creates a new container.
 | 
			
		|||
      -P, --publish-all             Publish all exposed ports to random ports
 | 
			
		||||
      -p, --publish=[]              Publish a container's port(s) to the host
 | 
			
		||||
      --pid=""                      PID namespace to use
 | 
			
		||||
      --pids-limit=-1                Tune container pids limit (set -1 for unlimited), kernel >= 4.3
 | 
			
		||||
      --privileged                  Give extended privileges to this container
 | 
			
		||||
      --read-only                   Mount the container's root filesystem as read only
 | 
			
		||||
      --restart="no"                Restart policy (no, on-failure[:max-retry], always, unless-stopped)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,6 +74,7 @@ parent = "smn_cli"
 | 
			
		|||
      -P, --publish-all             Publish all exposed ports to random ports
 | 
			
		||||
      -p, --publish=[]              Publish a container's port(s) to the host
 | 
			
		||||
      --pid=""                      PID namespace to use
 | 
			
		||||
      --pids-limit=-1                Tune container pids limit (set -1 for unlimited), kernel >= 4.3
 | 
			
		||||
      --privileged                  Give extended privileges to this container
 | 
			
		||||
      --read-only                   Mount the container's root filesystem as read only
 | 
			
		||||
      --restart="no"                Restart policy (no, on-failure[:max-retry], always, unless-stopped)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -956,3 +956,15 @@ func (s *DockerSuite) TestRunDeviceSymlink(c *check.C) {
 | 
			
		|||
	c.Assert(err, check.NotNil)
 | 
			
		||||
	c.Assert(strings.Trim(out, "\r\n"), checker.Contains, "not a device node", check.Commentf("expected output 'not a device node'"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestRunPidsLimit makes sure the pids cgroup is set with --pids-limit
 | 
			
		||||
func (s *DockerSuite) TestRunPidsLimit(c *check.C) {
 | 
			
		||||
	testRequires(c, pidsLimit)
 | 
			
		||||
 | 
			
		||||
	file := "/sys/fs/cgroup/pids/pids.max"
 | 
			
		||||
	out, _ := dockerCmd(c, "run", "--name", "skittles", "--pids-limit", "2", "busybox", "cat", file)
 | 
			
		||||
	c.Assert(strings.TrimSpace(out), checker.Equals, "2")
 | 
			
		||||
 | 
			
		||||
	out = inspectField(c, "skittles", "HostConfig.PidsLimit")
 | 
			
		||||
	c.Assert(out, checker.Equals, "2", check.Commentf("setting the pids limit failed"))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,12 @@ var (
 | 
			
		|||
		},
 | 
			
		||||
		"Test requires Oom control enabled.",
 | 
			
		||||
	}
 | 
			
		||||
	pidsLimit = testRequirement{
 | 
			
		||||
		func() bool {
 | 
			
		||||
			return SysInfo.PidsLimit
 | 
			
		||||
		},
 | 
			
		||||
		"Test requires pids limit enabled.",
 | 
			
		||||
	}
 | 
			
		||||
	kernelMemorySupport = testRequirement{
 | 
			
		||||
		func() bool {
 | 
			
		||||
			return SysInfo.KernelMemory
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,6 +58,7 @@ docker-create - Create a new container
 | 
			
		|||
[**-P**|**--publish-all**]
 | 
			
		||||
[**-p**|**--publish**[=*[]*]]
 | 
			
		||||
[**--pid**[=*[]*]]
 | 
			
		||||
[**--pids-limit**[=*PIDS_LIMIT*]]
 | 
			
		||||
[**--privileged**]
 | 
			
		||||
[**--read-only**]
 | 
			
		||||
[**--restart**[=*RESTART*]]
 | 
			
		||||
| 
						 | 
				
			
			@ -290,6 +291,9 @@ unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap.
 | 
			
		|||
     **host**: use the host's PID namespace inside the container.
 | 
			
		||||
     Note: the host mode gives the container full access to local PID and is therefore considered insecure.
 | 
			
		||||
 | 
			
		||||
**--pids-limit**=""
 | 
			
		||||
   Tune the container's pids limit. Set `-1` to have unlimited pids for the container.
 | 
			
		||||
 | 
			
		||||
**--privileged**=*true*|*false*
 | 
			
		||||
   Give extended privileges to this container. The default is *false*.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,7 @@ docker-run - Run a command in a new container
 | 
			
		|||
[**-P**|**--publish-all**]
 | 
			
		||||
[**-p**|**--publish**[=*[]*]]
 | 
			
		||||
[**--pid**[=*[]*]]
 | 
			
		||||
[**--pids-limit**[=*PIDS_LIMIT*]]
 | 
			
		||||
[**--privileged**]
 | 
			
		||||
[**--read-only**]
 | 
			
		||||
[**--restart**[=*RESTART*]]
 | 
			
		||||
| 
						 | 
				
			
			@ -420,6 +421,9 @@ Use `docker port` to see the actual mapping: `docker port CONTAINER $CONTAINERPO
 | 
			
		|||
     **host**: use the host's PID namespace inside the container.
 | 
			
		||||
     Note: the host mode gives the container full access to local PID and is therefore considered insecure.
 | 
			
		||||
 | 
			
		||||
**--pids-limit**=""
 | 
			
		||||
   Tune the container's pids limit. Set `-1` to have unlimited pids for the container.
 | 
			
		||||
 | 
			
		||||
**--uts**=*host*
 | 
			
		||||
   Set the UTS mode for the container
 | 
			
		||||
     **host**: use the host's UTS namespace inside the container.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ type SysInfo struct {
 | 
			
		|||
	cgroupCPUInfo
 | 
			
		||||
	cgroupBlkioInfo
 | 
			
		||||
	cgroupCpusetInfo
 | 
			
		||||
	cgroupPids
 | 
			
		||||
 | 
			
		||||
	// Whether IPv4 forwarding is supported or not, if this was disabled, networking will not work
 | 
			
		||||
	IPv4ForwardingDisabled bool
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +91,11 @@ type cgroupCpusetInfo struct {
 | 
			
		|||
	Mems string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type cgroupPids struct {
 | 
			
		||||
	// Whether Pids Limit is supported or not
 | 
			
		||||
	PidsLimit bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsCpusetCpusAvailable returns `true` if the provided string set is contained
 | 
			
		||||
// in cgroup's cpuset.cpus set, `false` otherwise.
 | 
			
		||||
// If error is not nil a parsing error occurred.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ func New(quiet bool) *SysInfo {
 | 
			
		|||
		sysInfo.cgroupCPUInfo = checkCgroupCPU(cgMounts, quiet)
 | 
			
		||||
		sysInfo.cgroupBlkioInfo = checkCgroupBlkioInfo(cgMounts, quiet)
 | 
			
		||||
		sysInfo.cgroupCpusetInfo = checkCgroupCpusetInfo(cgMounts, quiet)
 | 
			
		||||
		sysInfo.cgroupPids = checkCgroupPids(quiet)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, ok := cgMounts["devices"]
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +217,21 @@ func checkCgroupCpusetInfo(cgMounts map[string]string, quiet bool) cgroupCpusetI
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// checkCgroupPids reads the pids information from the pids cgroup mount point.
 | 
			
		||||
func checkCgroupPids(quiet bool) cgroupPids {
 | 
			
		||||
	_, err := cgroups.FindCgroupMountpoint("pids")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !quiet {
 | 
			
		||||
			logrus.Warn(err)
 | 
			
		||||
		}
 | 
			
		||||
		return cgroupPids{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return cgroupPids{
 | 
			
		||||
		PidsLimit: true,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cgroupEnabled(mountPoint, name string) bool {
 | 
			
		||||
	_, err := os.Stat(path.Join(mountPoint, name))
 | 
			
		||||
	return err == nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,6 +85,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host
 | 
			
		|||
		flIPv4Address       = cmd.String([]string{"-ip"}, "", "Container IPv4 address (e.g. 172.30.100.104)")
 | 
			
		||||
		flIPv6Address       = cmd.String([]string{"-ip6"}, "", "Container IPv6 address (e.g. 2001:db8::33)")
 | 
			
		||||
		flIpcMode           = cmd.String([]string{"-ipc"}, "", "IPC namespace to use")
 | 
			
		||||
		flPidsLimit         = cmd.Int64([]string{"-pids-limit"}, 0, "Tune container pids limit (set -1 for unlimited)")
 | 
			
		||||
		flRestartPolicy     = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits")
 | 
			
		||||
		flReadonlyRootfs    = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only")
 | 
			
		||||
		flLoggingDriver     = cmd.String([]string{"-log-driver"}, "", "Logging driver for container")
 | 
			
		||||
| 
						 | 
				
			
			@ -343,6 +344,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host
 | 
			
		|||
		CpusetCpus:           *flCpusetCpus,
 | 
			
		||||
		CpusetMems:           *flCpusetMems,
 | 
			
		||||
		CPUQuota:             *flCPUQuota,
 | 
			
		||||
		PidsLimit:            *flPidsLimit,
 | 
			
		||||
		BlkioWeight:          *flBlkioWeight,
 | 
			
		||||
		BlkioWeightDevice:    flBlkioWeightDevice.GetList(),
 | 
			
		||||
		BlkioDeviceReadBps:   flDeviceReadBps.GetList(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue