darkskysync 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- darkskysync/AbstractRemoteSyncAdapter.py +45 -0
- darkskysync/DarkSkySync.py +251 -0
- darkskysync/DarkSkySyncCLI.py +108 -0
- darkskysync/DataSourceFactory.py +268 -0
- darkskysync/Exceptions.py +27 -0
- darkskysync/LocalFileSystem.py +23 -0
- darkskysync/LocalSyncAdapter.py +59 -0
- darkskysync/RemoteFactory.py +59 -0
- darkskysync/RsyncWrapper.py +158 -0
- darkskysync/SSHClientAdapter.py +218 -0
- darkskysync/SSHWrapper.py +53 -0
- darkskysync/SshLoginInfo.py +33 -0
- darkskysync/__init__.py +86 -0
- darkskysync/data/config.template +31 -0
- darkskysync/docopt.py +577 -0
- darkskysync-0.4.0.dist-info/METADATA +61 -0
- darkskysync-0.4.0.dist-info/RECORD +22 -0
- darkskysync-0.4.0.dist-info/WHEEL +5 -0
- darkskysync-0.4.0.dist-info/entry_points.txt +2 -0
- darkskysync-0.4.0.dist-info/licenses/AUTHORS.rst +11 -0
- darkskysync-0.4.0.dist-info/licenses/LICENSE +13 -0
- darkskysync-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright (C) 2013 ETH Zurich, Institute of Astronomy
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Created on Sep 24, 2013
|
|
5
|
+
|
|
6
|
+
@author: J. Akeret
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from abc import ABCMeta, abstractmethod
|
|
12
|
+
|
|
13
|
+
from darkskysync.Exceptions import FileSystemError
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Created on Sep 30, 2013
|
|
17
|
+
|
|
18
|
+
@author: J. Akeret
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractRemoteSyncAdapter:
|
|
23
|
+
"""
|
|
24
|
+
Abstract class for all synchronization adapters
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__metaclass__ = ABCMeta
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def loadFiles(self, files):
|
|
31
|
+
"""
|
|
32
|
+
Loads files from the remote location
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def _prepareDestination(self, path):
|
|
37
|
+
if not os.path.isdir(path):
|
|
38
|
+
# We do not create *all* the parents, but we do create the
|
|
39
|
+
# directory if we can.
|
|
40
|
+
try:
|
|
41
|
+
os.makedirs(path)
|
|
42
|
+
except OSError as ex:
|
|
43
|
+
raise FileSystemError(
|
|
44
|
+
"Unable to create storage directory: " "%s" % (str(ex))
|
|
45
|
+
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Copyright (C) 2013 ETH Zurich, Institute of Astronomy
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Created on Sep 24, 2013
|
|
5
|
+
|
|
6
|
+
@author: J. Akeret
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import importlib.resources
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import darkskysync
|
|
16
|
+
|
|
17
|
+
from .DataSourceFactory import DataSourceFactory
|
|
18
|
+
from .Exceptions import (
|
|
19
|
+
ConfigurationError,
|
|
20
|
+
ConnectionError,
|
|
21
|
+
FileSystemError,
|
|
22
|
+
IllegalArgumentException,
|
|
23
|
+
)
|
|
24
|
+
from .LocalSyncAdapter import LocalSyncAdapter
|
|
25
|
+
from .RsyncWrapper import RsyncWrapper
|
|
26
|
+
from .SSHClientAdapter import SSHClientAdapter
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DarkSkySync:
|
|
30
|
+
"""
|
|
31
|
+
Helps to access and synchronize with a remote data storage. Features are:
|
|
32
|
+
- lists the available file structure on the remote host
|
|
33
|
+
- lists the file structure in the local cache
|
|
34
|
+
- synchronizes the local cache with the remote file structure
|
|
35
|
+
- allows for removing files from the local file system
|
|
36
|
+
|
|
37
|
+
It delegates the remote access to the according wrappers (currently either scp over
|
|
38
|
+
ssh or rsync over ssh) and manages the local file structure
|
|
39
|
+
|
|
40
|
+
:param template: [optional] alternate name for the config template to use
|
|
41
|
+
:param configFile: [optional] alternate config file to use
|
|
42
|
+
:param verbose: [optional] flag to enable verbose mode
|
|
43
|
+
|
|
44
|
+
:raises ConfigurationError: error while reading the config file
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
REMOTE_WRAPPER_TYPE_MAP = {"rsync": SSHClientAdapter, "ssh": SSHClientAdapter}
|
|
48
|
+
|
|
49
|
+
REMOTE_SYNC_WRAPPER_TYPE_MAP = {
|
|
50
|
+
"rsync": RsyncWrapper,
|
|
51
|
+
"ssh": SSHClientAdapter,
|
|
52
|
+
"local": LocalSyncAdapter,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
DEFAULT_TEMPLATE = "master"
|
|
56
|
+
|
|
57
|
+
def __init__(self, template=DEFAULT_TEMPLATE, configFile=None, verbose=False):
|
|
58
|
+
"""
|
|
59
|
+
Constructor
|
|
60
|
+
"""
|
|
61
|
+
self.configFile = configFile
|
|
62
|
+
|
|
63
|
+
if template is None:
|
|
64
|
+
template = DarkSkySync.DEFAULT_TEMPLATE
|
|
65
|
+
|
|
66
|
+
self._verifyConfigFile()
|
|
67
|
+
|
|
68
|
+
self.verbose = verbose
|
|
69
|
+
if self.verbose:
|
|
70
|
+
darkskysync.logger.info("Using config file: %s" % self.configFile)
|
|
71
|
+
|
|
72
|
+
dsFactory = DataSourceFactory.fromConfig(self.configFile)
|
|
73
|
+
self.dataSourceConfig = dsFactory.createDataSource(template)
|
|
74
|
+
|
|
75
|
+
def avail(self, path=None):
|
|
76
|
+
"""
|
|
77
|
+
Lists the available file structure on the remote host
|
|
78
|
+
|
|
79
|
+
:param path: [optional] sub path to directory structure to check
|
|
80
|
+
|
|
81
|
+
:return: list of file available on remote host
|
|
82
|
+
|
|
83
|
+
:raises ConnectionError: remote host could not be accessed
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
RemoteWrapper = DarkSkySync.REMOTE_WRAPPER_TYPE_MAP[
|
|
88
|
+
self.dataSourceConfig.remote.type
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
remoteWrapper = RemoteWrapper(self.dataSourceConfig)
|
|
92
|
+
fileList = remoteWrapper.getRemoteFilesList(path)
|
|
93
|
+
return self._filterFileList(fileList)
|
|
94
|
+
except ConnectionError as ex:
|
|
95
|
+
darkskysync.logger.error(str(ex))
|
|
96
|
+
raise ex
|
|
97
|
+
|
|
98
|
+
def list(self, path=None, recursive=False):
|
|
99
|
+
"""
|
|
100
|
+
Lists the available files in the local cache
|
|
101
|
+
|
|
102
|
+
:param path: [optional] sub path to directory structure to check
|
|
103
|
+
:param recursive: [optional] flag indicating if the cache should be browsed
|
|
104
|
+
recursively
|
|
105
|
+
|
|
106
|
+
:return: list of files in the local cache
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
filePath = self.dataSourceConfig.local.filePath
|
|
111
|
+
if path is not None:
|
|
112
|
+
filePath = os.path.join(filePath, path)
|
|
113
|
+
|
|
114
|
+
fileList = []
|
|
115
|
+
|
|
116
|
+
if not os.path.exists(filePath):
|
|
117
|
+
return fileList
|
|
118
|
+
|
|
119
|
+
if recursive:
|
|
120
|
+
for dirpath, dirnames, filenames in os.walk(filePath):
|
|
121
|
+
for fileName in filenames:
|
|
122
|
+
fileList.append(os.path.join(dirpath, fileName))
|
|
123
|
+
else:
|
|
124
|
+
pathContent = os.listdir(filePath)
|
|
125
|
+
fileList = [os.path.join(filePath, file) for file in pathContent]
|
|
126
|
+
|
|
127
|
+
return fileList
|
|
128
|
+
|
|
129
|
+
def load(self, names, dry_run=False, force=False):
|
|
130
|
+
"""
|
|
131
|
+
Loads the given file names. Checks first if the requested files are available on
|
|
132
|
+
the file system. If not, they will be downloaded from the remote host.
|
|
133
|
+
|
|
134
|
+
:param names: a list of names which should be loaded
|
|
135
|
+
:param dry_run: [optional] flag indicating a dry run. No files will be
|
|
136
|
+
downloaded
|
|
137
|
+
:param force: [optional] flag indicating that the files should be downloaded
|
|
138
|
+
even if they already exist on the local file system
|
|
139
|
+
|
|
140
|
+
:retun: list of files loaded from the remote host
|
|
141
|
+
|
|
142
|
+
:raises ConnectionError: remote host could not be accessed
|
|
143
|
+
:raises FileSystemError: error while accesing the local file system
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
RemoteWrapper = DarkSkySync.REMOTE_SYNC_WRAPPER_TYPE_MAP[
|
|
149
|
+
self.dataSourceConfig.remote.type
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
remoteWrapper = RemoteWrapper(
|
|
153
|
+
self.dataSourceConfig,
|
|
154
|
+
dry_run=dry_run,
|
|
155
|
+
force=force,
|
|
156
|
+
verbose=self.verbose,
|
|
157
|
+
)
|
|
158
|
+
loadedFiles = remoteWrapper.loadFiles(names)
|
|
159
|
+
return self._postProcessFileList(loadedFiles)
|
|
160
|
+
except ConnectionError as ex:
|
|
161
|
+
darkskysync.logger.error(str(ex))
|
|
162
|
+
raise ex
|
|
163
|
+
except FileSystemError as ex:
|
|
164
|
+
darkskysync.logger.error(str(ex))
|
|
165
|
+
raise ex
|
|
166
|
+
|
|
167
|
+
def remove(self, files=None, allFiles=False):
|
|
168
|
+
"""
|
|
169
|
+
Removes files and filetrees from the local cache
|
|
170
|
+
|
|
171
|
+
:param files: [optional] list of files or directories to delete
|
|
172
|
+
:param allFiles: [optional] flag indicating that all files shoudl be removed
|
|
173
|
+
from the cache
|
|
174
|
+
|
|
175
|
+
:raises FileSystemError: error while accessing the local file system
|
|
176
|
+
:raises IllegalArgumentException: Illegal parameter combination
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
if files is None and not allFiles:
|
|
180
|
+
darkskysync.logger.warning("At least a file has to be given")
|
|
181
|
+
raise IllegalArgumentException("At least a file has to be given")
|
|
182
|
+
|
|
183
|
+
if files is not None:
|
|
184
|
+
if isinstance(files, str):
|
|
185
|
+
files = [files]
|
|
186
|
+
for file in files:
|
|
187
|
+
path = os.path.join(self.dataSourceConfig.local.filePath, file)
|
|
188
|
+
if self.verbose:
|
|
189
|
+
darkskysync.logger.info("Removing: %s" % path)
|
|
190
|
+
|
|
191
|
+
if os.path.exists(path):
|
|
192
|
+
if os.path.isfile(path):
|
|
193
|
+
os.remove(path)
|
|
194
|
+
else:
|
|
195
|
+
shutil.rmtree(path)
|
|
196
|
+
|
|
197
|
+
if allFiles:
|
|
198
|
+
path = self.dataSourceConfig.local.filePath
|
|
199
|
+
if self.verbose:
|
|
200
|
+
darkskysync.logger.info("Removing content of: %s" % path)
|
|
201
|
+
for file in os.listdir(path):
|
|
202
|
+
filePath = os.path.join(path, file)
|
|
203
|
+
try:
|
|
204
|
+
if os.path.isfile(filePath):
|
|
205
|
+
os.unlink(filePath)
|
|
206
|
+
else:
|
|
207
|
+
shutil.rmtree(filePath)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
darkskysync.logger.warn(e)
|
|
210
|
+
raise FileSystemError(e)
|
|
211
|
+
|
|
212
|
+
def _verifyConfigFile(self):
|
|
213
|
+
if self.configFile is None:
|
|
214
|
+
self.configFile = DataSourceFactory.DEFAULT_CONFIGURATION_FILE
|
|
215
|
+
if not os.path.isfile(self.configFile):
|
|
216
|
+
if not os.path.exists(os.path.dirname(self.configFile)):
|
|
217
|
+
os.mkdir(os.path.dirname(self.configFile))
|
|
218
|
+
|
|
219
|
+
with importlib.resources.as_file(
|
|
220
|
+
importlib.resources.files(__package__).joinpath(
|
|
221
|
+
"data/config.template"
|
|
222
|
+
)
|
|
223
|
+
) as template:
|
|
224
|
+
darkskysync.logger.warning(
|
|
225
|
+
"Deploying default configuration file to %s.", self.configFile
|
|
226
|
+
)
|
|
227
|
+
shutil.copyfile(template, self.configFile)
|
|
228
|
+
|
|
229
|
+
else:
|
|
230
|
+
# Exit if supplied configuration file does not exists.
|
|
231
|
+
if not os.path.isfile(self.configFile):
|
|
232
|
+
sys.stderr.write(
|
|
233
|
+
"Unable to read configuration file `%s`.\n" % self.configFile
|
|
234
|
+
)
|
|
235
|
+
raise ConfigurationError(
|
|
236
|
+
"Unable to read configuration file `%s`.\n" % self.configFile
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _filterFileList(self, fileList):
|
|
240
|
+
fileList = [f for f in fileList if not f.startswith(".")]
|
|
241
|
+
fileList.sort()
|
|
242
|
+
return fileList
|
|
243
|
+
|
|
244
|
+
def _postProcessFileList(self, fileList):
|
|
245
|
+
files = []
|
|
246
|
+
for filePath in fileList:
|
|
247
|
+
if os.path.exists(filePath):
|
|
248
|
+
if not os.path.isfile(filePath):
|
|
249
|
+
filePath = filePath + "/"
|
|
250
|
+
files.append(filePath)
|
|
251
|
+
return files
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#! /usr/bin/env python
|
|
2
|
+
# Copyright (C) 2013 ETH Zurich, Institute of Astronomy
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
This is the DarkSkySync command line interface
|
|
6
|
+
|
|
7
|
+
Created on Sep 23, 2013
|
|
8
|
+
|
|
9
|
+
@author: J. Akeret
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
DarkSkySync avail [<path>] [--config=<file>] [--template=<template>] [-v|--verbose]
|
|
14
|
+
DarkSkySync list [<path>] [-r|--recursive] [--config=<file>] [--template=<template>] [-v|--verbose]
|
|
15
|
+
DarkSkySync load <name>... [--dry_run] [-f|--force] [--config=<file>] [--template=<template>] [-v|--verbose]
|
|
16
|
+
DarkSkySync remove (<name>...|--all) [--config=<file>] [--template=<template>] [-v|--verbose]
|
|
17
|
+
DarkSkySync -h | --help
|
|
18
|
+
DarkSkySync --version
|
|
19
|
+
|
|
20
|
+
Options
|
|
21
|
+
-h --help Show this screen.
|
|
22
|
+
--version Show version.
|
|
23
|
+
--config=<file> The configfile to use.
|
|
24
|
+
--template=<template> The template to use [default:master]
|
|
25
|
+
--all All files
|
|
26
|
+
-v --verbose More output
|
|
27
|
+
-f --force Force the download
|
|
28
|
+
-r --recursive List the directories in a recursive way
|
|
29
|
+
--dry_run Dry run - Not loading any files
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
from . import __version__
|
|
35
|
+
from .DarkSkySync import DarkSkySync
|
|
36
|
+
from .docopt import docopt
|
|
37
|
+
from .Exceptions import ConfigurationError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DarkSkySyncCLI:
|
|
41
|
+
"""
|
|
42
|
+
Command line interface for the darkskysync
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def launch(self):
|
|
49
|
+
"""
|
|
50
|
+
Starts the darkskysync by parsing the args and the config
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
args = docopt(__doc__, version="DarkSkySync CLI {}".format(__version__))
|
|
54
|
+
|
|
55
|
+
# print args
|
|
56
|
+
try:
|
|
57
|
+
dam = DarkSkySync(
|
|
58
|
+
template=args["--template"],
|
|
59
|
+
configFile=args["--config"],
|
|
60
|
+
verbose=args["--verbose"],
|
|
61
|
+
)
|
|
62
|
+
self.dispatch(dam, args)
|
|
63
|
+
except ConfigurationError:
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
def dispatch(self, dam, args):
|
|
67
|
+
"""
|
|
68
|
+
Dispatches the sub command to the given darkskysync instance
|
|
69
|
+
|
|
70
|
+
:param dam: the instance of the darkskysync to us
|
|
71
|
+
:param args: the arguments passed by the used
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
if args["avail"]:
|
|
75
|
+
filesList = dam.avail(path=args["<path>"])
|
|
76
|
+
self._printFilesList("Available files:", filesList)
|
|
77
|
+
|
|
78
|
+
elif args["list"]:
|
|
79
|
+
filesList = dam.list(path=args["<path>"], recursive=args["--recursive"])
|
|
80
|
+
self._printFilesList("Available files:", filesList)
|
|
81
|
+
|
|
82
|
+
elif args["load"]:
|
|
83
|
+
filesList = dam.load(args["<name>"], args["--dry_run"], args["--force"])
|
|
84
|
+
self._printFilesList("Loaded files:", filesList)
|
|
85
|
+
|
|
86
|
+
elif args["remove"]:
|
|
87
|
+
dam.remove(args["<name>"], args["--all"])
|
|
88
|
+
|
|
89
|
+
except Exception:
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
def _printFilesList(self, message, filesList):
|
|
93
|
+
print(message)
|
|
94
|
+
if len(filesList) > 0:
|
|
95
|
+
for fileName in filesList:
|
|
96
|
+
print(fileName)
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
print("-")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
cli = DarkSkySyncCLI()
|
|
104
|
+
cli.launch()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Copyright (C) 2013 ETH Zurich, Institute of Astronomy
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Created on Sep 23, 2013
|
|
5
|
+
|
|
6
|
+
@author: J. Akeret
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from configobj import ConfigObj
|
|
14
|
+
from voluptuous import All, Invalid, Length, Schema, Url, message
|
|
15
|
+
|
|
16
|
+
from .Exceptions import ConfigurationError
|
|
17
|
+
|
|
18
|
+
# from .LocalFileSystem import LocalFileSystem
|
|
19
|
+
from .RemoteFactory import SSHRemoteLocationFactory
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DataSource:
|
|
24
|
+
name: str
|
|
25
|
+
local: str
|
|
26
|
+
remote: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class LocalFileSystem:
|
|
31
|
+
name: str
|
|
32
|
+
filePath: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DataSourceFactory:
|
|
36
|
+
"""
|
|
37
|
+
A factory creating DataSource configurations
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
DEFAULT_CONFIGURATION_FILE = os.path.expanduser("~/.darkskysync/config")
|
|
41
|
+
|
|
42
|
+
REMOTE_FACTORY_TYPE_MAP = {
|
|
43
|
+
"rsync": SSHRemoteLocationFactory,
|
|
44
|
+
"ssh": SSHRemoteLocationFactory,
|
|
45
|
+
"local": SSHRemoteLocationFactory,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def __init__(self, config):
|
|
49
|
+
"""
|
|
50
|
+
Constructor
|
|
51
|
+
"""
|
|
52
|
+
self.config = config
|
|
53
|
+
validator = ConfigValidator(self.config)
|
|
54
|
+
validator.validate()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def fromConfig(cls, configfile):
|
|
58
|
+
"""
|
|
59
|
+
Helper method to initialize DataSourceFactory from an ini file.
|
|
60
|
+
:param configfile: path to the ini file
|
|
61
|
+
:return: configurator object
|
|
62
|
+
"""
|
|
63
|
+
config_reader = ConfigReader(configfile)
|
|
64
|
+
conf = config_reader.read_config()
|
|
65
|
+
return DataSourceFactory(conf)
|
|
66
|
+
|
|
67
|
+
def createDataSource(self, template, name=None):
|
|
68
|
+
"""
|
|
69
|
+
Creates a data source by inspecting the configuration properties of the
|
|
70
|
+
given data source template.
|
|
71
|
+
:param template: name of the data source template
|
|
72
|
+
|
|
73
|
+
:param name: name of the data source. If not defined, the data source
|
|
74
|
+
will be named after the template.
|
|
75
|
+
|
|
76
|
+
:return: :py:class:`DataSource` instance
|
|
77
|
+
|
|
78
|
+
:raises ConfigurationError: data source template not found in config
|
|
79
|
+
"""
|
|
80
|
+
if not name:
|
|
81
|
+
name = template
|
|
82
|
+
|
|
83
|
+
if template not in self.config:
|
|
84
|
+
raise ConfigurationError(
|
|
85
|
+
"Invalid configuration for data source `%s`: %s" "" % (template, name)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
conf = self.config[template]
|
|
89
|
+
|
|
90
|
+
extra = conf["datasource"].copy()
|
|
91
|
+
extra.pop("local")
|
|
92
|
+
extra.pop("remote")
|
|
93
|
+
|
|
94
|
+
return DataSource(
|
|
95
|
+
template, self.createLocal(template), self.createRemote(template)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def createLocal(self, template):
|
|
99
|
+
conf = self.config[template]
|
|
100
|
+
|
|
101
|
+
return LocalFileSystem(conf["datasource"]["local"], conf["local"]["path"])
|
|
102
|
+
|
|
103
|
+
def createRemote(self, template):
|
|
104
|
+
conf = self.config[template]
|
|
105
|
+
remoteType = conf["remote"]["type"]
|
|
106
|
+
|
|
107
|
+
RemoteFactory = DataSourceFactory.REMOTE_FACTORY_TYPE_MAP[remoteType]
|
|
108
|
+
|
|
109
|
+
factory = RemoteFactory()
|
|
110
|
+
|
|
111
|
+
return factory.create(conf["datasource"]["remote"], conf)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ConfigValidator:
|
|
115
|
+
def __init__(self, config):
|
|
116
|
+
self.config = config
|
|
117
|
+
|
|
118
|
+
def _pre_validate(self):
|
|
119
|
+
"""
|
|
120
|
+
Handles all pre validation phase functionality, such as:
|
|
121
|
+
- reading environment variables
|
|
122
|
+
- interpolating configuraiton options
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def _post_validate(self):
|
|
126
|
+
"""
|
|
127
|
+
Handles all post validation phase functionality, such as:
|
|
128
|
+
- expanding file paths
|
|
129
|
+
"""
|
|
130
|
+
# expand all paths
|
|
131
|
+
for dataSource, values in self.config.items():
|
|
132
|
+
conf = self.config[dataSource]
|
|
133
|
+
|
|
134
|
+
privkey = os.path.expanduser(values["login"]["known_hosts"])
|
|
135
|
+
conf["login"]["known_hosts"] = privkey
|
|
136
|
+
|
|
137
|
+
privkey = os.path.expanduser(values["login"]["user_key_private"])
|
|
138
|
+
conf["login"]["user_key_private"] = privkey
|
|
139
|
+
|
|
140
|
+
pubkey = os.path.expanduser(values["login"]["user_key_public"])
|
|
141
|
+
conf["login"]["user_key_public"] = pubkey
|
|
142
|
+
|
|
143
|
+
path = os.path.expanduser(values["local"]["path"])
|
|
144
|
+
conf["local"]["path"] = path
|
|
145
|
+
|
|
146
|
+
def validate(self):
|
|
147
|
+
"""
|
|
148
|
+
Validates the given configuration :py:attr:`self.config` to comply.
|
|
149
|
+
As well all types are converted to the expected
|
|
150
|
+
format if possible.
|
|
151
|
+
"""
|
|
152
|
+
self._pre_validate()
|
|
153
|
+
|
|
154
|
+
# custom validators
|
|
155
|
+
@message("file could not be found")
|
|
156
|
+
def check_file(v):
|
|
157
|
+
f = os.path.expanduser(os.path.expanduser(v))
|
|
158
|
+
if os.path.exists(f):
|
|
159
|
+
return f
|
|
160
|
+
else:
|
|
161
|
+
raise Invalid("file could not be found `%s`" % v)
|
|
162
|
+
|
|
163
|
+
# schema to validate all dataSource properties
|
|
164
|
+
schema = {
|
|
165
|
+
"datasource": {
|
|
166
|
+
"local": All(str, Length(min=1)),
|
|
167
|
+
"remote": All(str, Length(min=1)),
|
|
168
|
+
},
|
|
169
|
+
"local": {
|
|
170
|
+
"path": All(str, Length(min=1)), # check_file(),
|
|
171
|
+
},
|
|
172
|
+
"login": {
|
|
173
|
+
"name": All(str, Length(min=1)),
|
|
174
|
+
"known_hosts": check_file(),
|
|
175
|
+
"user_key_private": check_file(),
|
|
176
|
+
"user_key_public": check_file(),
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
remote_schema_ssh = {
|
|
181
|
+
"type": "ssh",
|
|
182
|
+
"host": Url(str),
|
|
183
|
+
"port": All(str, Length(min=1)),
|
|
184
|
+
"path": All(str, Length(min=0)),
|
|
185
|
+
"login": All(str, Length(min=1)),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# validation
|
|
189
|
+
validator = Schema(schema, required=True, extra=True)
|
|
190
|
+
ssh_validator = Schema(remote_schema_ssh, required=True, extra=False)
|
|
191
|
+
|
|
192
|
+
if not self.config:
|
|
193
|
+
raise Invalid("No data sources found in configuration.")
|
|
194
|
+
|
|
195
|
+
for dataSource, properties in self.config.items():
|
|
196
|
+
validator(properties)
|
|
197
|
+
|
|
198
|
+
if properties["remote"]["type"] == "ssh":
|
|
199
|
+
ssh_validator(properties["remote"])
|
|
200
|
+
|
|
201
|
+
self._post_validate()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ConfigReader:
|
|
205
|
+
"""
|
|
206
|
+
Reads the configuration properties from a ini file.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
local_section = "local"
|
|
210
|
+
remote_section = "remote"
|
|
211
|
+
login_section = "login"
|
|
212
|
+
datasource_section = "datasource"
|
|
213
|
+
|
|
214
|
+
def __init__(self, configfile):
|
|
215
|
+
"""
|
|
216
|
+
:param configfile: path to configfile
|
|
217
|
+
"""
|
|
218
|
+
self.configfile = configfile
|
|
219
|
+
self.conf = ConfigObj(self.configfile, interpolation=False)
|
|
220
|
+
|
|
221
|
+
def read_config(self):
|
|
222
|
+
"""
|
|
223
|
+
Reads the configuration properties from the ini file and links the
|
|
224
|
+
section to comply with the datasource config dictionary format.
|
|
225
|
+
:return: dictionary containing all configuration properties from the
|
|
226
|
+
ini file in compliance to the datasource config format
|
|
227
|
+
:raises MultipleInvalid: not all sections present or broken links
|
|
228
|
+
between secitons
|
|
229
|
+
"""
|
|
230
|
+
datasources = {
|
|
231
|
+
key: value
|
|
232
|
+
for key, value in self.conf.items()
|
|
233
|
+
if (
|
|
234
|
+
re.search(ConfigReader.datasource_section + "/(.*)", key)
|
|
235
|
+
and key.count("/") == 1
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
conf_values = dict()
|
|
240
|
+
|
|
241
|
+
for datasource in datasources:
|
|
242
|
+
name = re.search(
|
|
243
|
+
ConfigReader.datasource_section + "/(.*)", datasource
|
|
244
|
+
).groups()[0]
|
|
245
|
+
try:
|
|
246
|
+
datasource_conf = dict(self.conf[datasource])
|
|
247
|
+
local_name = ConfigReader.local_section + "/" + datasource_conf["local"]
|
|
248
|
+
remote_name = (
|
|
249
|
+
ConfigReader.remote_section + "/" + datasource_conf["remote"]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
values = dict()
|
|
253
|
+
values["datasource"] = datasource_conf
|
|
254
|
+
values["local"] = dict(self.conf[local_name])
|
|
255
|
+
values["remote"] = remote_conf = dict(self.conf[remote_name])
|
|
256
|
+
|
|
257
|
+
login_name = ConfigReader.login_section + "/" + remote_conf["login"]
|
|
258
|
+
|
|
259
|
+
values["login"] = dict(self.conf[login_name])
|
|
260
|
+
|
|
261
|
+
conf_values[name] = values
|
|
262
|
+
except KeyError:
|
|
263
|
+
raise Exception(
|
|
264
|
+
"could not find all sections required `datasource`, "
|
|
265
|
+
"`setup`, `login`, `cloud` for datasource `%s`" % name
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return conf_values
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Copyright (C) 2013 ETH Zurich, Institute of Astronomy
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Created on Sep 23, 2013
|
|
5
|
+
|
|
6
|
+
@author: J. Akeret
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConnectionError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileSystemError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IllegalArgumentException(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FileNotFoundError(Exception):
|
|
27
|
+
pass
|