2019-09-30 10:53:51 -04:00
// Copyright (c) Huawei Technologies Co., Ltd. 2018-2019. All rights reserved.
2020-01-21 17:00:00 +08:00
// syscontainer-tools is licensed under the Mulan PSL v1.
2019-09-30 10:53:51 -04:00
// You can use this software according to the terms and conditions of the Mulan PSL v1.
// You may obtain a copy of Mulan PSL v1 at:
// http://license.coscl.org.cn/MulanPSL
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
// PURPOSE.
// See the Mulan PSL v1 for more details.
// Description: device operation lib
// Author: zhangwei
// Create: 2018-01-18
package libdevice
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
2020-01-21 17:00:00 +08:00
hconfig "isula.org/syscontainer-tools/config"
"isula.org/syscontainer-tools/container"
"isula.org/syscontainer-tools/libdevice/nsexec"
"isula.org/syscontainer-tools/pkg/udevd"
"isula.org/syscontainer-tools/types"
"isula.org/syscontainer-tools/utils"
2019-09-30 10:53:51 -04:00
)
func checkDevice ( config hconfig . ContainerConfig , devs [ ] * types . Device , opts * types . AddDeviceOptions ) error {
devices := devs
// check all devices, config updated
if len ( devs ) == 0 {
for _ , dm := range config . GetAllDevices ( ) {
devices = append ( devices , & types . Device {
Type : dm . Type ,
Major : dm . Major ,
Minor : dm . Minor ,
PathOnHost : dm . PathOnHost ,
Path : dm . PathInContainer ,
} )
}
}
checkDeviceQos := func ( qos * types . Qos ) bool {
for _ , device := range devices {
if device . Major == qos . Major && device . Minor == qos . Minor {
return true
}
}
return false
}
qosOpts := append ( opts . ReadBps , opts . WriteBps ... )
qosOpts = append ( qosOpts , opts . ReadIOPS ... )
qosOpts = append ( qosOpts , opts . WriteIOPS ... )
for _ , opt := range qosOpts {
if ! checkDeviceQos ( opt ) {
return fmt . Errorf ( "device %v was not added to container or not in add-device args" , opt . Path )
}
}
return nil
}
// UpdateDeviceOwner update device owner
func UpdateDeviceOwner ( spec * specs . Spec , device * types . Device ) {
if spec == nil {
return
}
uid , gid := utils . GetUIDGid ( spec )
if uid != - 1 {
device . UID = uint32 ( uid )
}
if gid != - 1 {
device . GID = uint32 ( gid )
}
}
// AddDevice will add devices to a container.
func AddDevice ( c * container . Container , devices [ ] * types . Device , opts * types . AddDeviceOptions ) error {
driver := nsexec . NewDefaultNsDriver ( )
pid := strconv . Itoa ( c . Pid ( ) )
innerPath , err := c . GetCgroupPath ( )
if err != nil {
return err
}
cgroupPath , err := FindCgroupPath ( pid , "devices" , innerPath )
if err != nil {
return err
}
for _ , device := range devices {
UpdateDeviceOwner ( c . GetSpec ( ) , device )
}
udevdCtrl := udevd . NewUdevdController ( )
// lockFile := <container config path>/lock
// 1. use file lock, to make sure only one process to access this config file.
// 2. different container has different lock, will not block other container.
if err := c . Lock ( ) ; err != nil {
return err
}
defer c . Unlock ( )
// create config file handler.
config , err := hconfig . NewContainerConfig ( c )
if err != nil {
return err
}
if err := checkDevice ( config , devices , opts ) ; err != nil {
return err
}
defer func ( ) {
if err := config . Flush ( ) ; err != nil {
logrus . Infof ( "config Flush error:%v" , err )
}
} ( )
if err := udevdCtrl . Lock ( ) ; err != nil {
return err
}
defer udevdCtrl . Unlock ( )
if err := udevdCtrl . LoadRules ( ) ; err != nil {
return err
}
defer udevdCtrl . ToDisk ( )
var retErr [ ] error
// add device and udpate cgroup here
for _ , device := range devices {
// update config here
if err = config . UpdateDevice ( device , true ) ; err != nil {
retErr = append ( retErr , err )
continue
}
r := & udevd . Rule {
Name : device . PathOnHost ,
Container : c . ContainerID ( ) ,
CtrDevName : device . Path ,
}
if device . Type != "c" {
devType , err := types . GetDeviceType ( device . PathOnHost )
if err != nil {
retErr = append ( retErr , err )
config . UpdateDevice ( device , false )
continue
}
if devType == "disk" {
udevdCtrl . AddRule ( r )
}
}
// Do not insert device and update cgroup when:
// 1. update-config-only flag is set
// 2. container isn't running (pid==0)
if ! opts . UpdateConfigOnly && c . Pid ( ) > 0 && c . CheckPidExist ( ) {
// add device to container.
if err = driver . AddDevice ( pid , device , opts . Force ) ; err != nil {
retErr = append ( retErr , err )
// roll back config and udev rules
config . UpdateDevice ( device , false )
udevdCtrl . RemoveRule ( r )
continue
}
// update cgroup access permission.
if err = UpdateCgroupPermission ( cgroupPath , device , true ) ; err != nil {
retErr = append ( retErr , err )
// roll back config and udev rules and remove device
driver . RemoveDevice ( pid , device )
config . UpdateDevice ( device , false )
udevdCtrl . RemoveRule ( r )
continue
}
}
fmt . Fprintf ( os . Stdout , "Add device (%s) to container(%s,%s) done.\n" , device . PathOnHost , c . Name ( ) , device . Path )
logrus . Infof ( "Add device (%s) to container(%s,%s) done" , device . PathOnHost , c . Name ( ) , device . Path )
}
if err := updateQos ( config , pid , innerPath , opts ) ; err != nil {
return err
}
if len ( retErr ) == 0 {
return nil
}
for i := 0 ; i < len ( retErr ) ; i ++ {
retErr [ i ] = fmt . Errorf ( "%s" , retErr [ i ] . Error ( ) )
}
return errors . New ( strings . Trim ( fmt . Sprint ( retErr ) , "[]" ) )
}
// UpdateDevice will update device for container.
func UpdateDevice ( c * container . Container , opts * types . AddDeviceOptions ) error {
pid := strconv . Itoa ( c . Pid ( ) )
innerPath , err := c . GetCgroupPath ( )
if err != nil {
return err
}
if err := c . Lock ( ) ; err != nil {
return err
}
defer c . Unlock ( )
// create config file handler.
config , err := hconfig . NewContainerConfig ( c )
if err != nil {
return err
}
if err := checkDevice ( config , [ ] * types . Device { } , opts ) ; err != nil {
return err
}
defer func ( ) {
if err := config . Flush ( ) ; err != nil {
logrus . Infof ( "config Flush error:%v" , err )
}
} ( )
if err := updateQos ( config , pid , innerPath , opts ) ; err != nil {
return err
}
return nil
}
// RemoveDevice will remove devices from container
func RemoveDevice ( c * container . Container , devices [ ] * types . Device , followPartition bool ) error {
driver := nsexec . NewDefaultNsDriver ( )
pid := strconv . Itoa ( c . Pid ( ) )
innerPath , err := c . GetCgroupPath ( )
if err != nil {
return err
}
cgroupPath , err := FindCgroupPath ( pid , "devices" , innerPath )
if err != nil {
return err
}
if err := c . Lock ( ) ; err != nil {
return err
}
defer c . Unlock ( )
config , err := hconfig . NewContainerConfig ( c )
if err != nil {
return err
}
defer config . Flush ( )
udevdCtrl := udevd . NewUdevdController ( )
if err := udevdCtrl . Lock ( ) ; err != nil {
return err
}
defer udevdCtrl . Unlock ( )
if err := udevdCtrl . LoadRules ( ) ; err != nil {
return err
}
defer udevdCtrl . ToDisk ( )
var retErr [ ] error
var newDevices [ ] * types . Device
for _ , device := range devices {
newDevice := config . FindDeviceByMapping ( device )
if newDevice == nil {
2020-01-21 17:00:00 +08:00
errinfo := fmt . Sprint ( "Device pair(" , device . PathOnHost , ":" , device . Path , ") is not added by syscontainer-tools, can not remove it, please check input parameter." )
2019-09-30 10:53:51 -04:00
retErr = append ( retErr , errors . New ( errinfo ) )
continue
}
newDevices = append ( newDevices , newDevice )
if followPartition {
subDevices := config . FindSubPartition ( newDevice )
for _ , subDev := range subDevices {
2020-01-21 17:00:00 +08:00
// check the sub partition is added by syscontainer-tools
2019-09-30 10:53:51 -04:00
if found := config . FindDeviceByMapping ( subDev ) ; found == nil {
continue
}
found := false
for _ , eDev := range newDevices {
if subDev . Path == eDev . Path && subDev . PathOnHost == eDev . PathOnHost {
found = true
break
}
}
if ! found {
newDevices = append ( newDevices , subDev )
}
}
}
}
for _ , device := range newDevices {
// update config.
if err = config . UpdateDevice ( device , false ) ; err != nil {
retErr = append ( retErr , err )
continue
}
r := & udevd . Rule {
Name : device . PathOnHost ,
CtrDevName : device . Path ,
Container : c . ContainerID ( ) ,
}
udevdCtrl . RemoveRule ( r )
// only update for running container
if c . Pid ( ) > 0 && c . CheckPidExist ( ) {
if err = driver . RemoveDevice ( pid , device ) ; err != nil {
config . UpdateDevice ( device , true )
if device . Type != "c" {
devType , err := types . GetDeviceType ( device . PathOnHost )
if err != nil {
retErr = append ( retErr , err )
config . UpdateDevice ( device , true )
continue
}
if devType == "disk" {
udevdCtrl . AddRule ( r )
}
}
retErr = append ( retErr , err )
continue
}
// update cgroup access permission.
if err = UpdateCgroupPermission ( cgroupPath , device , false ) ; err != nil {
// TODO: also need a roll back?
retErr = append ( retErr , err )
continue
}
}
fmt . Fprintf ( os . Stdout , "Remove device (%s) from container(%s,%s) done.\n" , device . PathOnHost , c . Name ( ) , device . Path )
logrus . Infof ( "Remove device (%s) from container(%s,%s) done.\n" , device . PathOnHost , c . Name ( ) , device . Path )
if err := removeQos ( config , pid , innerPath , device ) ; err != nil {
retErr = append ( retErr , err )
}
}
if len ( retErr ) == 0 {
return nil
}
for i := 0 ; i < len ( retErr ) ; i ++ {
retErr [ i ] = fmt . Errorf ( "%s" , retErr [ i ] . Error ( ) )
}
return errors . New ( strings . Trim ( fmt . Sprint ( retErr ) , "[]" ) )
}
// ListDevice list container devices
func ListDevice ( c * container . Container ) ( [ ] * hconfig . DeviceMapping , [ ] * hconfig . DeviceMapping , error ) {
if err := c . Lock ( ) ; err != nil {
return nil , nil , err
}
defer c . Unlock ( )
hConfig , err := hconfig . NewContainerConfig ( c )
if err != nil {
return nil , nil , err
}
allDevice := hConfig . GetAllDevices ( )
var majorDevices [ ] * hconfig . DeviceMapping
for _ , sDevice := range allDevice {
if sDevice . Parent == "" {
majorDevices = append ( majorDevices , sDevice )
}
}
return allDevice , majorDevices , nil
}
// AddPath will add paths from host to container
func AddPath ( c * container . Container , binds [ ] * types . Bind ) error {
driver := nsexec . NewDefaultNsDriver ( )
pid := strconv . Itoa ( c . Pid ( ) )
if err := c . Lock ( ) ; err != nil {
return fmt . Errorf ( "AddPath: failed to get lock, err: %s" , err )
}
defer c . Unlock ( )
config , err := hconfig . NewContainerConfig ( c )
if err != nil {
return fmt . Errorf ( "AddPath: failed to create config, err: %s" , err )
}
defer config . Flush ( )
if err := config . CheckPathNum ( ) ; err != nil {
return err
}
var retErr [ ] error
for _ , bind := range binds {
logrus . Debugf ( "Adding path: %+v" , bind )
// 1. update config.
hostPathExist , err := config . UpdateBind ( bind , true )
if err != nil {
return fmt . Errorf ( "AddPath: failed to UpdateBind, err: %s" , err )
}
if c . Pid ( ) > 0 && c . CheckPidExist ( ) {
// 2. prepare transferpath if needed
if err := utils . PrepareTransferPath ( "/" , c . ContainerID ( ) , bind , ! hostPathExist ) ; err != nil {
config . UpdateBind ( bind , false )
// if no existed, do unmount
if ! hostPathExist {
utils . RemoveTransferPath ( c . ContainerID ( ) , bind )
}
return fmt . Errorf ( "AddPath: failed to prepare transfer base, err: %s" , err )
}
if err = driver . AddBind ( pid , bind ) ; err != nil {
retErr = append ( retErr , err )
config . UpdateBind ( bind , false )
if ! hostPathExist {
utils . RemoveTransferPath ( c . ContainerID ( ) , bind )
}
return fmt . Errorf ( "AddPath: failed to add bind, err: %s" , err )
}
}
msg := fmt . Sprintf ( "Add path (%s) to container(%s,%s) done." , bind . HostPath , c . Name ( ) , bind . ContainerPath )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
if len ( retErr ) == 0 {
return nil
}
for i := 0 ; i < len ( retErr ) ; i ++ {
retErr [ i ] = fmt . Errorf ( "%s" , retErr [ i ] . Error ( ) )
}
return errors . New ( strings . Trim ( fmt . Sprint ( retErr ) , "[]" ) )
}
// RemovePath will remove paths from container
func RemovePath ( c * container . Container , binds [ ] * types . Bind ) error {
driver := nsexec . NewDefaultNsDriver ( )
pid := strconv . Itoa ( c . Pid ( ) )
if err := c . Lock ( ) ; err != nil {
return err
}
defer c . Unlock ( )
config , err := hconfig . NewContainerConfig ( c )
if err != nil {
return err
}
defer config . Flush ( )
var retErr [ ] error
for _ , bind := range binds {
mp , err := config . GetBindInConfig ( bind )
if err != nil {
retErr = append ( retErr , err )
continue
}
if mp == nil {
2020-01-21 17:00:00 +08:00
errinfo := fmt . Sprint ( "Path pair(" , bind . HostPath , ":" , bind . ContainerPath , ") is not added by syscontainer-tools, can not remove it, please check input parameter" )
2019-09-30 10:53:51 -04:00
retErr = append ( retErr , errors . New ( errinfo ) )
continue
}
bind . MountOption = mp . Permission
// update config.
removeHostPath , err := config . UpdateBind ( bind , false )
if err != nil {
retErr = append ( retErr , fmt . Errorf ( "Failed to update bind(%v), error: %s, still try to remove it" , bind , err ) )
}
// remove from container.
if c . Pid ( ) > 0 && c . CheckPidExist ( ) {
if err := driver . RemoveBind ( pid , bind ) ; err != nil {
retErr = append ( retErr , fmt . Errorf ( "Failed to remove bind(%v),error: %s" , bind , err ) )
config . UpdateBind ( bind , true )
continue
}
if removeHostPath == true {
if err := utils . RemoveTransferPath ( c . ContainerID ( ) , bind ) ; err != nil {
retErr = append ( retErr , fmt . Errorf ( "Remove path (%s) from %s failed, err: %s" , bind . HostPath , c . Name ( ) , err ) )
}
}
}
msg := fmt . Sprintf ( "Remove path (%s) from container(%s,%s) done" , bind . HostPath , c . Name ( ) , bind . ContainerPath )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
if len ( retErr ) == 0 {
return nil
}
for i := 0 ; i < len ( retErr ) ; i ++ {
retErr [ i ] = fmt . Errorf ( "%s" , retErr [ i ] . Error ( ) )
}
return errors . New ( strings . Trim ( fmt . Sprint ( retErr ) , "[]" ) )
}
// ListPath list container paths
func ListPath ( ctr * container . Container ) ( [ ] string , error ) {
if err := ctr . Lock ( ) ; err != nil {
return nil , err
}
defer ctr . Unlock ( )
hConfig , err := hconfig . NewContainerConfig ( ctr )
if err != nil {
return nil , err
}
return hConfig . GetBinds ( ) , nil
}
func updateQos ( config hconfig . ContainerConfig , pid , innerPath string , opts * types . AddDeviceOptions ) error {
// update device read iops
for _ , devReadIOPS := range opts . ReadIOPS {
if err := UpdateCgroupDeviceReadIOPS ( pid , innerPath , devReadIOPS . String ( ) ) ; err != nil {
return err
}
if err := config . UpdateDeviceQos ( devReadIOPS , hconfig . QosReadIOPS ) ; err != nil {
return err
}
msg := fmt . Sprintf ( "Update read iops for device (%s,%s) done." , devReadIOPS . Path , devReadIOPS . Value )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
// update device write iops
for _ , devWriteIOPS := range opts . WriteIOPS {
if err := UpdateCgroupDeviceWriteIOPS ( pid , innerPath , devWriteIOPS . String ( ) ) ; err != nil {
return err
}
if err := config . UpdateDeviceQos ( devWriteIOPS , hconfig . QosWriteIOPS ) ; err != nil {
return err
}
msg := fmt . Sprintf ( "Update write iops for device (%s,%s) done." , devWriteIOPS . Path , devWriteIOPS . Value )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
// update device read bps
for _ , devReadBps := range opts . ReadBps {
if err := UpdateCgroupDeviceReadBps ( pid , innerPath , devReadBps . String ( ) ) ; err != nil {
return err
}
if err := config . UpdateDeviceQos ( devReadBps , hconfig . QosReadBps ) ; err != nil {
return err
}
msg := fmt . Sprintf ( "Update read bps for device (%s,%s) done." , devReadBps . Path , devReadBps . Value )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
// update device write bps
for _ , devWriteBps := range opts . WriteBps {
if err := UpdateCgroupDeviceWriteBps ( pid , innerPath , devWriteBps . String ( ) ) ; err != nil {
return err
}
if err := config . UpdateDeviceQos ( devWriteBps , hconfig . QosWriteBps ) ; err != nil {
return err
}
msg := fmt . Sprintf ( "Update write bps for device (%s,%s) done." , devWriteBps . Path , devWriteBps . Value )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
// update device blkio weight
for _ , devBlkioWeight := range opts . BlkioWeight {
cfqEnable , err := devBlkioWeight . GetCfqAbility ( )
if err == nil && cfqEnable {
if err := UpdateCgroupDeviceWeight ( pid , innerPath , devBlkioWeight . String ( ) ) ; err != nil {
return err
}
if err := config . UpdateDeviceQos ( devBlkioWeight , hconfig . QosBlkioWeight ) ; err != nil {
return err
}
msg := fmt . Sprintf ( "Update blkio weight for device (%s,%s) done." , devBlkioWeight . Path , devBlkioWeight . Value )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
} else {
msg := fmt . Sprintf ( "device not support cfq:%s" , devBlkioWeight . Path )
fmt . Fprintln ( os . Stdout , msg )
logrus . Info ( msg )
}
}
return nil
}
func removeQos ( config hconfig . ContainerConfig , pid , innerPath string , device * types . Device ) error {
cleanString := fmt . Sprintf ( "%d:%d 0" , device . Major , device . Minor )
if exist , err := config . RemoveDeviceQos ( device , hconfig . QosReadIOPS ) ; err != nil {
return err
} else if exist {
if err := UpdateCgroupDeviceReadIOPS ( pid , innerPath , cleanString ) ; err != nil {
return err
}
fmt . Fprintf ( os . Stdout , "Remove read iops for device (%s) done.\n" , device . PathOnHost )
}
if exist , err := config . RemoveDeviceQos ( device , hconfig . QosWriteIOPS ) ; err != nil {
return err
} else if exist {
if err := UpdateCgroupDeviceWriteIOPS ( pid , innerPath , cleanString ) ; err != nil {
return err
}
fmt . Fprintf ( os . Stdout , "Remove write iops for device (%s) done.\n" , device . PathOnHost )
}
if exist , err := config . RemoveDeviceQos ( device , hconfig . QosReadBps ) ; err != nil {
return err
} else if exist {
if err := UpdateCgroupDeviceReadBps ( pid , innerPath , cleanString ) ; err != nil {
return err
}
fmt . Fprintf ( os . Stdout , "Remove read bps for device (%s) done.\n" , device . PathOnHost )
}
if exist , err := config . RemoveDeviceQos ( device , hconfig . QosWriteBps ) ; err != nil {
return err
} else if exist {
if err := UpdateCgroupDeviceWriteBps ( pid , innerPath , cleanString ) ; err != nil {
return err
}
fmt . Fprintf ( os . Stdout , "Remove write bps for device (%s) done.\n" , device . PathOnHost )
}
if exist , err := config . RemoveDeviceQos ( device , hconfig . QosBlkioWeight ) ; err != nil {
return err
} else if exist {
if err := UpdateCgroupDeviceWeight ( pid , innerPath , cleanString ) ; err != nil {
return err
}
fmt . Fprintf ( os . Stdout , "Remove blkio weight for device (%s) done.\n" , device . PathOnHost )
}
return nil
}