1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
|
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package instance
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"github.com/sylabs/singularity/v4/internal/pkg/util/user"
"github.com/sylabs/singularity/v4/pkg/syfs"
)
const (
// SingSubDir represents directory where Singularity instance files are stored
SingSubDir = "sing"
// LogSubDir represents directory where Singularity instance log files are stored
LogSubDir = "logs"
)
const (
// ProgPrefix is the prefix used by a singularity instance process
ProgPrefix = "Singularity instance"
instancePath = "instances"
authorizedChars = `^[a-zA-Z0-9._-]+$`
prognameFormat = "%s: %s [%s]"
)
// File represents an instance file storing instance information
type File struct {
Path string `json:"-"`
Pid int `json:"pid"`
PPid int `json:"ppid"`
Name string `json:"name"`
User string `json:"user"`
Image string `json:"image"`
Config []byte `json:"config"`
UserNs bool `json:"userns"`
Cgroup bool `json:"cgroup"`
IP string `json:"ip"`
LogErrPath string `json:"logErrPath"`
LogOutPath string `json:"logOutPath"`
}
// ProcName returns processus name based on instance name
// and username
func ProcName(name string, username string) (string, error) {
if err := CheckName(name); err != nil {
return "", fmt.Errorf("while checking instance name: %s", err)
}
if username == "" {
return "", fmt.Errorf("while getting instance processus name: empty username")
}
return fmt.Sprintf(prognameFormat, ProgPrefix, username, name), nil
}
// ExtractName extracts instance name from an instance:// URI
func ExtractName(name string) string {
return strings.Replace(name, "instance://", "", 1)
}
// CheckName checks if name is a valid instance name
func CheckName(name string) error {
r := regexp.MustCompile(authorizedChars)
if !r.MatchString(name) {
return fmt.Errorf("%s is not a valid instance name", name)
}
return nil
}
// getPath returns the path where searching for instance files
func getPath(username string, subDir string) (string, error) {
hostname, err := os.Hostname()
if err != nil {
return "", err
}
var u *user.User
if username == "" {
u, err = user.CurrentOriginal()
} else {
u, err = user.GetPwNam(username)
}
if err != nil {
return "", err
}
configDir, err := syfs.ConfigDirForUsername(u.Name)
if err != nil {
return "", err
}
return filepath.Join(configDir, instancePath, subDir, hostname, u.Name), nil
}
// GetDir returns directory where instances file will be stored
func GetDir(name string, subDir string) (string, error) {
if err := CheckName(name); err != nil {
return "", err
}
path, err := getPath("", subDir)
if err != nil {
return "", err
}
return filepath.Join(path, name), nil
}
// Get returns the instance file corresponding to instance name
func Get(name string, subDir string) (*File, error) {
if err := CheckName(name); err != nil {
return nil, err
}
list, err := List("", name, subDir)
if err != nil {
return nil, err
}
if len(list) != 1 {
return nil, fmt.Errorf("no instance found with name %s", name)
}
return list[0], nil
}
// Add creates an instance file for a named instance in a privileged
// or unprivileged path
func Add(name string, subDir string) (*File, error) {
if err := CheckName(name); err != nil {
return nil, err
}
_, err := Get(name, subDir)
if err == nil {
return nil, fmt.Errorf("instance %s already exists", name)
}
i := &File{Name: name}
i.Path, err = getPath("", subDir)
if err != nil {
return nil, err
}
jsonFile := name + ".json"
i.Path = filepath.Join(i.Path, name, jsonFile)
return i, nil
}
// List returns instance files matching username and/or name pattern
func List(username string, name string, subDir string) ([]*File, error) {
list := make([]*File, 0)
path, err := getPath(username, subDir)
if err != nil {
return nil, err
}
pattern := filepath.Join(path, name, name+".json")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
for _, file := range files {
r, err := os.Open(file)
if os.IsNotExist(err) {
continue
} else if err != nil {
return nil, err
}
f := &File{}
if err := json.NewDecoder(r).Decode(f); err != nil {
r.Close()
return nil, err
}
r.Close()
f.Path = file
// delete ghost singularity instance files
if subDir == SingSubDir && f.isExited() {
f.Delete()
continue
}
list = append(list, f)
}
return list, nil
}
// Delete deletes instance file
func (i *File) Delete() error {
dir := filepath.Dir(i.Path)
if dir == "." {
dir = ""
}
return os.RemoveAll(dir)
}
// isExited returns if the instance process is exited or not.
func (i *File) isExited() bool {
if i.PPid <= 0 {
return true
}
// if instance is not running anymore, automatically
// delete instance files after checking that instance
// parent process
err := syscall.Kill(i.PPid, 0)
if err == syscall.ESRCH {
return true
} else if err == nil {
// process is alive and is owned by you otherwise
// we would have obtained permission denied error,
// now check if it's an instance parent process
cmdline := fmt.Sprintf("/proc/%d/cmdline", i.PPid)
d, err := os.ReadFile(cmdline)
if err != nil {
// this is racy and not accurate but as the process
// may have exited during above read, check again
// for process presence
return syscall.Kill(i.PPid, 0) == syscall.ESRCH
}
// not an instance master process
return !strings.HasPrefix(string(d), ProgPrefix)
}
return false
}
// Update stores instance information in associated instance file
func (i *File) Update() error {
b, err := json.Marshal(i)
if err != nil {
return err
}
path := filepath.Dir(i.Path)
oldumask := syscall.Umask(0)
defer syscall.Umask(oldumask)
if err := os.MkdirAll(path, 0o700); err != nil {
return err
}
file, err := os.OpenFile(i.Path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY|syscall.O_NOFOLLOW, 0o644)
if err != nil {
return err
}
defer file.Close()
if _, err := file.Write(b); err != nil {
return fmt.Errorf("failed to write instance file %s: %s", i.Path, err)
}
return file.Sync()
}
// GetLogFilePaths returns the paths of log files containing
// .err, .out streams, respectively
func GetLogFilePaths(name string, subDir string) (string, string, error) {
path, err := getPath("", subDir)
if err != nil {
return "", "", err
}
logErrPath := filepath.Join(path, name+".err")
logOutPath := filepath.Join(path, name+".out")
return logErrPath, logOutPath, nil
}
// SetLogFile replaces stdout/stderr streams and redirect content
// to log file
func SetLogFile(name string, uid int, subDir string) (*os.File, *os.File, error) {
path, err := getPath("", subDir)
if err != nil {
return nil, nil, err
}
stderrPath := filepath.Join(path, name+".err")
stdoutPath := filepath.Join(path, name+".out")
oldumask := syscall.Umask(0)
defer syscall.Umask(oldumask)
if err := os.MkdirAll(filepath.Dir(stderrPath), 0o700); err != nil {
return nil, nil, err
}
if err := os.MkdirAll(filepath.Dir(stdoutPath), 0o700); err != nil {
return nil, nil, err
}
stderr, err := os.OpenFile(stderrPath, os.O_RDWR|os.O_CREATE|os.O_APPEND|syscall.O_NOFOLLOW, 0o644)
if err != nil {
return nil, nil, err
}
stdout, err := os.OpenFile(stdoutPath, os.O_RDWR|os.O_CREATE|os.O_APPEND|syscall.O_NOFOLLOW, 0o644)
if err != nil {
return nil, nil, err
}
if uid != os.Getuid() || uid == 0 {
if err := stderr.Chown(uid, os.Getgid()); err != nil {
return nil, nil, err
}
if err := stdout.Chown(uid, os.Getgid()); err != nil {
return nil, nil, err
}
}
return stdout, stderr, nil
}
|